mirror of
https://github.com/ansible/awx.git
synced 2026-04-26 20:25:24 -02:30
Compare commits
101 Commits
21.10.1
...
serve-ansi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e77f89e01f | ||
|
|
68f56570ae | ||
|
|
61821faa00 | ||
|
|
c26d211ee0 | ||
|
|
5163795cc0 | ||
|
|
ea9c52aca6 | ||
|
|
a7ebce1fef | ||
|
|
5de9cf748d | ||
|
|
ebea78943d | ||
|
|
1e33bc4020 | ||
|
|
e9ad01e806 | ||
|
|
8a4059d266 | ||
|
|
01a7076267 | ||
|
|
32b6aec66b | ||
|
|
884ab424d5 | ||
|
|
7e55305c45 | ||
|
|
e9a1582b70 | ||
|
|
51ef1e808d | ||
|
|
11fbfc2063 | ||
|
|
f6395c69dd | ||
|
|
ca07bc85cb | ||
|
|
b87dd6dc56 | ||
|
|
f8d46d5e71 | ||
|
|
ce0a456ecc | ||
|
|
5775ff1422 | ||
|
|
82e8bcd2bb | ||
|
|
d73cc501d5 | ||
|
|
7e40a4daed | ||
|
|
47e824dd11 | ||
|
|
4643b816fe | ||
|
|
79d9329cfa | ||
|
|
6492c03965 | ||
|
|
98107301a5 | ||
|
|
4810099158 | ||
|
|
1aca9929ab | ||
|
|
2aa58bc17d | ||
|
|
be4b826259 | ||
|
|
b99a434dee | ||
|
|
6cee99a9f9 | ||
|
|
ee509aea56 | ||
|
|
b5452a48f8 | ||
|
|
68e555824d | ||
|
|
0c980fa7d5 | ||
|
|
e34ce8c795 | ||
|
|
58bad6cfa9 | ||
|
|
3543644e0e | ||
|
|
36c0d07b30 | ||
|
|
03b0281fde | ||
|
|
6f6f04a071 | ||
|
|
239827a9cf | ||
|
|
ac9871b36f | ||
|
|
f739908ccf | ||
|
|
cf1ec07eab | ||
|
|
d968b648de | ||
|
|
5dd0eab806 | ||
|
|
41f3f381ec | ||
|
|
ac8cff75ce | ||
|
|
94b34b801c | ||
|
|
8f6849fc22 | ||
|
|
821b1701bf | ||
|
|
b7f2825909 | ||
|
|
e87e041a2a | ||
|
|
cc336e791c | ||
|
|
c2a3c3b285 | ||
|
|
7b8dcc98e7 | ||
|
|
d5011492bf | ||
|
|
e363ddf470 | ||
|
|
987709cdb3 | ||
|
|
f04ac3c798 | ||
|
|
71a6baccdb | ||
|
|
d07076b686 | ||
|
|
7129f3e8cd | ||
|
|
df61a5cea1 | ||
|
|
a4b950f79b | ||
|
|
8be739d255 | ||
|
|
ca54195099 | ||
|
|
f0fcfdde39 | ||
|
|
80b1ba4a35 | ||
|
|
51f8e362dc | ||
|
|
737d6d8c8b | ||
|
|
beaf6b6058 | ||
|
|
aad1fbcef8 | ||
|
|
0b96d617ac | ||
|
|
fe768a159b | ||
|
|
c1ebea858b | ||
|
|
7b2938f515 | ||
|
|
e524d3df3e | ||
|
|
cec2d2dfb9 | ||
|
|
15b7ad3570 | ||
|
|
36ff9cbc6d | ||
|
|
ed74d80ecb | ||
|
|
4a7f4d0ed4 | ||
|
|
6e08c3567f | ||
|
|
58734a33c4 | ||
|
|
2832f28014 | ||
|
|
d34f6af830 | ||
|
|
f9bb26ad33 | ||
|
|
878035c13b | ||
|
|
2cc971a43f | ||
|
|
9d77c54612 | ||
|
|
ef651a3a21 |
17
.github/triage_replies.md
vendored
17
.github/triage_replies.md
vendored
@@ -53,6 +53,16 @@ https://github.com/ansible/awx/#get-involved \
|
|||||||
Thank you once again for this and your interest in AWX!
|
Thank you once again for this and your interest in AWX!
|
||||||
|
|
||||||
|
|
||||||
|
### Red Hat Support Team
|
||||||
|
- Hi! \
|
||||||
|
\
|
||||||
|
It appears that you are using an RPM build for RHEL. Please reach out to the Red Hat support team and submit a ticket. \
|
||||||
|
\
|
||||||
|
Here is the link to do so: \
|
||||||
|
\
|
||||||
|
https://access.redhat.com/support \
|
||||||
|
\
|
||||||
|
Thank you for your submission and for supporting AWX!
|
||||||
|
|
||||||
|
|
||||||
## Common
|
## Common
|
||||||
@@ -96,6 +106,13 @@ The Ansible Community is looking at building an EE that corresponds to all of th
|
|||||||
### Oracle AWX
|
### Oracle AWX
|
||||||
We'd be happy to help if you can reproduce this with AWX since we do not have Oracle's Linux Automation Manager. If you need help with this specific version of Oracles Linux Automation Manager you will need to contact your Oracle for support.
|
We'd be happy to help if you can reproduce this with AWX since we do not have Oracle's Linux Automation Manager. If you need help with this specific version of Oracles Linux Automation Manager you will need to contact your Oracle for support.
|
||||||
|
|
||||||
|
### Community Resolved
|
||||||
|
Hi,
|
||||||
|
|
||||||
|
We are happy to see that it appears a fix has been provided for your issue, so we will go ahead and close this ticket. Please feel free to reopen if any other problems arise.
|
||||||
|
|
||||||
|
<name of community member who helped> thanks so much for taking the time to write a thoughtful and helpful response to this issue!
|
||||||
|
|
||||||
### AWX Release
|
### AWX Release
|
||||||
Subject: Announcing AWX Xa.Ya.za and AWX-Operator Xb.Yb.zb
|
Subject: Announcing AWX Xa.Ya.za and AWX-Operator Xb.Yb.zb
|
||||||
|
|
||||||
|
|||||||
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
@@ -145,3 +145,22 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
AWX_TEST_IMAGE: awx
|
AWX_TEST_IMAGE: awx
|
||||||
AWX_TEST_VERSION: ci
|
AWX_TEST_VERSION: ci
|
||||||
|
|
||||||
|
collection-sanity:
|
||||||
|
name: awx_collection sanity
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# The containers that GitHub Actions use have Ansible installed, so upgrade to make sure we have the latest version.
|
||||||
|
- name: Upgrade ansible-core
|
||||||
|
run: python3 -m pip install --upgrade ansible-core
|
||||||
|
|
||||||
|
- name: Run sanity tests
|
||||||
|
run: make test_collection_sanity
|
||||||
|
env:
|
||||||
|
# needed due to cgroupsv2. This is fixed, but a stable release
|
||||||
|
# with the fix has not been made yet.
|
||||||
|
ANSIBLE_TEST_PREFER_PODMAN: 1
|
||||||
|
|||||||
4
.github/workflows/e2e_test.yml
vendored
4
.github/workflows/e2e_test.yml
vendored
@@ -6,7 +6,7 @@ env:
|
|||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [labeled]
|
types: [labeled]
|
||||||
jobs:
|
jobs:
|
||||||
e2e-test:
|
e2e-test:
|
||||||
if: contains(github.event.pull_request.labels.*.name, 'qe:e2e')
|
if: contains(github.event.pull_request.labels.*.name, 'qe:e2e')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -107,5 +107,3 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: AWX-logs-${{ matrix.job }}
|
name: AWX-logs-${{ matrix.job }}
|
||||||
path: make-docker-compose-output.log
|
path: make-docker-compose-output.log
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/pr_body_check.yml
vendored
6
.github/workflows/pr_body_check.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
PR_BODY: ${{ github.event.pull_request.body }}
|
PR_BODY: ${{ github.event.pull_request.body }}
|
||||||
run: |
|
run: |
|
||||||
echo $PR_BODY | grep "Bug, Docs Fix or other nominal change" > Z
|
echo "$PR_BODY" | grep "Bug, Docs Fix or other nominal change" > Z
|
||||||
echo $PR_BODY | grep "New or Enhanced Feature" > Y
|
echo "$PR_BODY" | grep "New or Enhanced Feature" > Y
|
||||||
echo $PR_BODY | grep "Breaking Change" > X
|
echo "$PR_BODY" | grep "Breaking Change" > X
|
||||||
exit 0
|
exit 0
|
||||||
# We exit 0 and set the shell to prevent the returns from the greps from failing this step
|
# We exit 0 and set the shell to prevent the returns from the greps from failing this step
|
||||||
# See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
|
# See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
|
||||||
|
|||||||
15
.github/workflows/promote.yml
vendored
15
.github/workflows/promote.yml
vendored
@@ -38,9 +38,13 @@ jobs:
|
|||||||
- name: Build collection and publish to galaxy
|
- name: Build collection and publish to galaxy
|
||||||
run: |
|
run: |
|
||||||
COLLECTION_TEMPLATE_VERSION=true COLLECTION_NAMESPACE=${{ env.collection_namespace }} make build_collection
|
COLLECTION_TEMPLATE_VERSION=true COLLECTION_NAMESPACE=${{ env.collection_namespace }} make build_collection
|
||||||
ansible-galaxy collection publish \
|
if [ "$(curl --head -sw '%{http_code}' https://galaxy.ansible.com/download/${{ env.collection_namespace }}-awx-${{ github.event.release.tag_name }}.tar.gz | tail -1)" == "302" ] ; then \
|
||||||
--token=${{ secrets.GALAXY_TOKEN }} \
|
echo "Galaxy release already done"; \
|
||||||
awx_collection_build/${{ env.collection_namespace }}-awx-${{ github.event.release.tag_name }}.tar.gz
|
else \
|
||||||
|
ansible-galaxy collection publish \
|
||||||
|
--token=${{ secrets.GALAXY_TOKEN }} \
|
||||||
|
awx_collection_build/${{ env.collection_namespace }}-awx-${{ github.event.release.tag_name }}.tar.gz; \
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Set official pypi info
|
- name: Set official pypi info
|
||||||
run: echo pypi_repo=pypi >> $GITHUB_ENV
|
run: echo pypi_repo=pypi >> $GITHUB_ENV
|
||||||
@@ -52,6 +56,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build awxkit and upload to pypi
|
- name: Build awxkit and upload to pypi
|
||||||
run: |
|
run: |
|
||||||
|
git reset --hard
|
||||||
cd awxkit && python3 setup.py bdist_wheel
|
cd awxkit && python3 setup.py bdist_wheel
|
||||||
twine upload \
|
twine upload \
|
||||||
-r ${{ env.pypi_repo }} \
|
-r ${{ env.pypi_repo }} \
|
||||||
@@ -74,4 +79,6 @@ jobs:
|
|||||||
docker tag ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} quay.io/${{ github.repository }}:latest
|
docker tag ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} quay.io/${{ github.repository }}:latest
|
||||||
docker push quay.io/${{ github.repository }}:${{ github.event.release.tag_name }}
|
docker push quay.io/${{ github.repository }}:${{ github.event.release.tag_name }}
|
||||||
docker push quay.io/${{ github.repository }}:latest
|
docker push quay.io/${{ github.repository }}:latest
|
||||||
|
docker pull ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }}
|
||||||
|
docker tag ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }} quay.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }}
|
||||||
|
docker push quay.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }}
|
||||||
|
|||||||
15
.github/workflows/stage.yml
vendored
15
.github/workflows/stage.yml
vendored
@@ -84,6 +84,20 @@ jobs:
|
|||||||
-e push=yes \
|
-e push=yes \
|
||||||
-e awx_official=yes
|
-e awx_official=yes
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
run: |
|
||||||
|
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||||
|
|
||||||
|
- name: Log in to Quay
|
||||||
|
run: |
|
||||||
|
echo ${{ secrets.QUAY_TOKEN }} | docker login quay.io -u ${{ secrets.QUAY_USER }} --password-stdin
|
||||||
|
|
||||||
|
- name: tag awx-ee:latest with version input
|
||||||
|
run: |
|
||||||
|
docker pull quay.io/ansible/awx-ee:latest
|
||||||
|
docker tag quay.io/ansible/awx-ee:latest ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }}
|
||||||
|
docker push ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }}
|
||||||
|
|
||||||
- name: Build and stage awx-operator
|
- name: Build and stage awx-operator
|
||||||
working-directory: awx-operator
|
working-directory: awx-operator
|
||||||
run: |
|
run: |
|
||||||
@@ -103,6 +117,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
AWX_TEST_IMAGE: ${{ github.repository }}
|
AWX_TEST_IMAGE: ${{ github.repository }}
|
||||||
AWX_TEST_VERSION: ${{ github.event.inputs.version }}
|
AWX_TEST_VERSION: ${{ github.event.inputs.version }}
|
||||||
|
AWX_EE_TEST_IMAGE: ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }}
|
||||||
|
|
||||||
- name: Create draft release for AWX
|
- name: Create draft release for AWX
|
||||||
working-directory: awx
|
working-directory: awx
|
||||||
|
|||||||
34
Makefile
34
Makefile
@@ -6,7 +6,20 @@ CHROMIUM_BIN=/tmp/chrome-linux/chrome
|
|||||||
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
MANAGEMENT_COMMAND ?= awx-manage
|
MANAGEMENT_COMMAND ?= awx-manage
|
||||||
VERSION := $(shell $(PYTHON) tools/scripts/scm_version.py)
|
VERSION := $(shell $(PYTHON) tools/scripts/scm_version.py)
|
||||||
COLLECTION_VERSION := $(shell $(PYTHON) tools/scripts/scm_version.py | cut -d . -f 1-3)
|
|
||||||
|
# ansible-test requires semver compatable version, so we allow overrides to hack it
|
||||||
|
COLLECTION_VERSION ?= $(shell $(PYTHON) tools/scripts/scm_version.py | cut -d . -f 1-3)
|
||||||
|
# args for the ansible-test sanity command
|
||||||
|
COLLECTION_SANITY_ARGS ?= --docker
|
||||||
|
# collection unit testing directories
|
||||||
|
COLLECTION_TEST_DIRS ?= awx_collection/test/awx
|
||||||
|
# collection integration test directories (defaults to all)
|
||||||
|
COLLECTION_TEST_TARGET ?=
|
||||||
|
# args for collection install
|
||||||
|
COLLECTION_PACKAGE ?= awx
|
||||||
|
COLLECTION_NAMESPACE ?= awx
|
||||||
|
COLLECTION_INSTALL = ~/.ansible/collections/ansible_collections/$(COLLECTION_NAMESPACE)/$(COLLECTION_PACKAGE)
|
||||||
|
COLLECTION_TEMPLATE_VERSION ?= false
|
||||||
|
|
||||||
# NOTE: This defaults the container image version to the branch that's active
|
# NOTE: This defaults the container image version to the branch that's active
|
||||||
COMPOSE_TAG ?= $(GIT_BRANCH)
|
COMPOSE_TAG ?= $(GIT_BRANCH)
|
||||||
@@ -288,19 +301,13 @@ test:
|
|||||||
cd awxkit && $(VENV_BASE)/awx/bin/tox -re py3
|
cd awxkit && $(VENV_BASE)/awx/bin/tox -re py3
|
||||||
awx-manage check_migrations --dry-run --check -n 'missing_migration_file'
|
awx-manage check_migrations --dry-run --check -n 'missing_migration_file'
|
||||||
|
|
||||||
COLLECTION_TEST_DIRS ?= awx_collection/test/awx
|
|
||||||
COLLECTION_TEST_TARGET ?=
|
|
||||||
COLLECTION_PACKAGE ?= awx
|
|
||||||
COLLECTION_NAMESPACE ?= awx
|
|
||||||
COLLECTION_INSTALL = ~/.ansible/collections/ansible_collections/$(COLLECTION_NAMESPACE)/$(COLLECTION_PACKAGE)
|
|
||||||
COLLECTION_TEMPLATE_VERSION ?= false
|
|
||||||
|
|
||||||
test_collection:
|
test_collection:
|
||||||
rm -f $(shell ls -d $(VENV_BASE)/awx/lib/python* | head -n 1)/no-global-site-packages.txt
|
rm -f $(shell ls -d $(VENV_BASE)/awx/lib/python* | head -n 1)/no-global-site-packages.txt
|
||||||
if [ "$(VENV_BASE)" ]; then \
|
if [ "$(VENV_BASE)" ]; then \
|
||||||
. $(VENV_BASE)/awx/bin/activate; \
|
. $(VENV_BASE)/awx/bin/activate; \
|
||||||
fi && \
|
fi && \
|
||||||
pip install ansible-core && \
|
if ! [ -x "$(shell command -v ansible-playbook)" ]; then pip install ansible-core; fi
|
||||||
|
ansible --version
|
||||||
py.test $(COLLECTION_TEST_DIRS) -v
|
py.test $(COLLECTION_TEST_DIRS) -v
|
||||||
# The python path needs to be modified so that the tests can find Ansible within the container
|
# The python path needs to be modified so that the tests can find Ansible within the container
|
||||||
# First we will use anything expility set as PYTHONPATH
|
# First we will use anything expility set as PYTHONPATH
|
||||||
@@ -330,8 +337,13 @@ install_collection: build_collection
|
|||||||
rm -rf $(COLLECTION_INSTALL)
|
rm -rf $(COLLECTION_INSTALL)
|
||||||
ansible-galaxy collection install awx_collection_build/$(COLLECTION_NAMESPACE)-$(COLLECTION_PACKAGE)-$(COLLECTION_VERSION).tar.gz
|
ansible-galaxy collection install awx_collection_build/$(COLLECTION_NAMESPACE)-$(COLLECTION_PACKAGE)-$(COLLECTION_VERSION).tar.gz
|
||||||
|
|
||||||
test_collection_sanity: install_collection
|
test_collection_sanity:
|
||||||
cd $(COLLECTION_INSTALL) && ansible-test sanity
|
rm -rf awx_collection_build/
|
||||||
|
rm -rf $(COLLECTION_INSTALL)
|
||||||
|
if ! [ -x "$(shell command -v ansible-test)" ]; then pip install ansible-core; fi
|
||||||
|
ansible --version
|
||||||
|
COLLECTION_VERSION=1.0.0 make install_collection
|
||||||
|
cd $(COLLECTION_INSTALL) && ansible-test sanity $(COLLECTION_SANITY_ARGS)
|
||||||
|
|
||||||
test_collection_integration: install_collection
|
test_collection_integration: install_collection
|
||||||
cd $(COLLECTION_INSTALL) && ansible-test integration $(COLLECTION_TEST_TARGET)
|
cd $(COLLECTION_INSTALL) && ansible-test integration $(COLLECTION_TEST_TARGET)
|
||||||
|
|||||||
@@ -96,6 +96,15 @@ register(
|
|||||||
category=_('Authentication'),
|
category=_('Authentication'),
|
||||||
category_slug='authentication',
|
category_slug='authentication',
|
||||||
)
|
)
|
||||||
|
register(
|
||||||
|
'ALLOW_METRICS_FOR_ANONYMOUS_USERS',
|
||||||
|
field_class=fields.BooleanField,
|
||||||
|
default=False,
|
||||||
|
label=_('Allow anonymous users to poll metrics'),
|
||||||
|
help_text=_('If true, anonymous users are allowed to poll metrics.'),
|
||||||
|
category=_('Authentication'),
|
||||||
|
category_slug='authentication',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def authentication_validate(serializer, attrs):
|
def authentication_validate(serializer, attrs):
|
||||||
|
|||||||
@@ -344,6 +344,13 @@ class InstanceDetail(RetrieveUpdateAPIView):
|
|||||||
model = models.Instance
|
model = models.Instance
|
||||||
serializer_class = serializers.InstanceSerializer
|
serializer_class = serializers.InstanceSerializer
|
||||||
|
|
||||||
|
def update_raw_data(self, data):
|
||||||
|
# these fields are only valid on creation of an instance, so they unwanted on detail view
|
||||||
|
data.pop('listener_port', None)
|
||||||
|
data.pop('node_type', None)
|
||||||
|
data.pop('hostname', None)
|
||||||
|
return super(InstanceDetail, self).update_raw_data(data)
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
r = super(InstanceDetail, self).update(request, *args, **kwargs)
|
r = super(InstanceDetail, self).update(request, *args, **kwargs)
|
||||||
if status.is_success(r.status_code):
|
if status.is_success(r.status_code):
|
||||||
|
|||||||
@@ -5,9 +5,11 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
|
from django.conf import settings
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
# Django REST Framework
|
# Django REST Framework
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
@@ -31,9 +33,14 @@ class MetricsView(APIView):
|
|||||||
|
|
||||||
renderer_classes = [renderers.PlainTextRenderer, renderers.PrometheusJSONRenderer, renderers.BrowsableAPIRenderer]
|
renderer_classes = [renderers.PlainTextRenderer, renderers.PrometheusJSONRenderer, renderers.BrowsableAPIRenderer]
|
||||||
|
|
||||||
|
def initialize_request(self, request, *args, **kwargs):
|
||||||
|
if settings.ALLOW_METRICS_FOR_ANONYMOUS_USERS:
|
||||||
|
self.permission_classes = (AllowAny,)
|
||||||
|
return super(APIView, self).initialize_request(request, *args, **kwargs)
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
'''Show Metrics Details'''
|
'''Show Metrics Details'''
|
||||||
if request.user.is_superuser or request.user.is_system_auditor:
|
if settings.ALLOW_METRICS_FOR_ANONYMOUS_USERS or request.user.is_superuser or request.user.is_system_auditor:
|
||||||
metrics_to_show = ''
|
metrics_to_show = ''
|
||||||
if not request.query_params.get('subsystemonly', "0") == "1":
|
if not request.query_params.get('subsystemonly', "0") == "1":
|
||||||
metrics_to_show += metrics().decode('UTF-8')
|
metrics_to_show += metrics().decode('UTF-8')
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from rest_framework import status
|
|||||||
|
|
||||||
from awx.main.constants import ACTIVE_STATES
|
from awx.main.constants import ACTIVE_STATES
|
||||||
from awx.main.utils import get_object_or_400
|
from awx.main.utils import get_object_or_400
|
||||||
from awx.main.models.ha import Instance, InstanceGroup
|
from awx.main.models.ha import Instance, InstanceGroup, schedule_policy_task
|
||||||
from awx.main.models.organization import Team
|
from awx.main.models.organization import Team
|
||||||
from awx.main.models.projects import Project
|
from awx.main.models.projects import Project
|
||||||
from awx.main.models.inventory import Inventory
|
from awx.main.models.inventory import Inventory
|
||||||
@@ -107,6 +107,11 @@ class InstanceGroupMembershipMixin(object):
|
|||||||
if inst_name in ig_obj.policy_instance_list:
|
if inst_name in ig_obj.policy_instance_list:
|
||||||
ig_obj.policy_instance_list.pop(ig_obj.policy_instance_list.index(inst_name))
|
ig_obj.policy_instance_list.pop(ig_obj.policy_instance_list.index(inst_name))
|
||||||
ig_obj.save(update_fields=['policy_instance_list'])
|
ig_obj.save(update_fields=['policy_instance_list'])
|
||||||
|
|
||||||
|
# sometimes removing an instance has a non-obvious consequence
|
||||||
|
# this is almost always true if policy_instance_percentage or _minimum is non-zero
|
||||||
|
# after removing a single instance, the other memberships need to be re-balanced
|
||||||
|
schedule_policy_task()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2697,46 +2697,66 @@ class ActivityStreamAccess(BaseAccess):
|
|||||||
# 'job_template', 'job', 'project', 'project_update', 'workflow_job',
|
# 'job_template', 'job', 'project', 'project_update', 'workflow_job',
|
||||||
# 'inventory_source', 'workflow_job_template'
|
# 'inventory_source', 'workflow_job_template'
|
||||||
|
|
||||||
inventory_set = Inventory.accessible_objects(self.user, 'read_role')
|
q = Q(user=self.user)
|
||||||
credential_set = Credential.accessible_objects(self.user, 'read_role')
|
inventory_set = Inventory.accessible_pk_qs(self.user, 'read_role')
|
||||||
|
if inventory_set:
|
||||||
|
q |= (
|
||||||
|
Q(ad_hoc_command__inventory__in=inventory_set)
|
||||||
|
| Q(inventory__in=inventory_set)
|
||||||
|
| Q(host__inventory__in=inventory_set)
|
||||||
|
| Q(group__inventory__in=inventory_set)
|
||||||
|
| Q(inventory_source__inventory__in=inventory_set)
|
||||||
|
| Q(inventory_update__inventory_source__inventory__in=inventory_set)
|
||||||
|
)
|
||||||
|
|
||||||
|
credential_set = Credential.accessible_pk_qs(self.user, 'read_role')
|
||||||
|
if credential_set:
|
||||||
|
q |= Q(credential__in=credential_set)
|
||||||
|
|
||||||
auditing_orgs = (
|
auditing_orgs = (
|
||||||
(Organization.accessible_objects(self.user, 'admin_role') | Organization.accessible_objects(self.user, 'auditor_role'))
|
(Organization.accessible_objects(self.user, 'admin_role') | Organization.accessible_objects(self.user, 'auditor_role'))
|
||||||
.distinct()
|
.distinct()
|
||||||
.values_list('id', flat=True)
|
.values_list('id', flat=True)
|
||||||
)
|
)
|
||||||
project_set = Project.accessible_objects(self.user, 'read_role')
|
if auditing_orgs:
|
||||||
jt_set = JobTemplate.accessible_objects(self.user, 'read_role')
|
q |= (
|
||||||
team_set = Team.accessible_objects(self.user, 'read_role')
|
Q(user__in=auditing_orgs.values('member_role__members'))
|
||||||
wfjt_set = WorkflowJobTemplate.accessible_objects(self.user, 'read_role')
|
| Q(organization__in=auditing_orgs)
|
||||||
app_set = OAuth2ApplicationAccess(self.user).filtered_queryset()
|
| Q(notification_template__organization__in=auditing_orgs)
|
||||||
token_set = OAuth2TokenAccess(self.user).filtered_queryset()
|
| Q(notification__notification_template__organization__in=auditing_orgs)
|
||||||
|
| Q(label__organization__in=auditing_orgs)
|
||||||
|
| Q(role__in=Role.objects.filter(ancestors__in=self.user.roles.all()) if auditing_orgs else [])
|
||||||
|
)
|
||||||
|
|
||||||
return qs.filter(
|
project_set = Project.accessible_pk_qs(self.user, 'read_role')
|
||||||
Q(ad_hoc_command__inventory__in=inventory_set)
|
if project_set:
|
||||||
| Q(o_auth2_application__in=app_set)
|
q |= Q(project__in=project_set) | Q(project_update__project__in=project_set)
|
||||||
| Q(o_auth2_access_token__in=token_set)
|
|
||||||
| Q(user__in=auditing_orgs.values('member_role__members'))
|
jt_set = JobTemplate.accessible_pk_qs(self.user, 'read_role')
|
||||||
| Q(user=self.user)
|
if jt_set:
|
||||||
| Q(organization__in=auditing_orgs)
|
q |= Q(job_template__in=jt_set) | Q(job__job_template__in=jt_set)
|
||||||
| Q(inventory__in=inventory_set)
|
|
||||||
| Q(host__inventory__in=inventory_set)
|
wfjt_set = WorkflowJobTemplate.accessible_pk_qs(self.user, 'read_role')
|
||||||
| Q(group__inventory__in=inventory_set)
|
if wfjt_set:
|
||||||
| Q(inventory_source__inventory__in=inventory_set)
|
q |= (
|
||||||
| Q(inventory_update__inventory_source__inventory__in=inventory_set)
|
Q(workflow_job_template__in=wfjt_set)
|
||||||
| Q(credential__in=credential_set)
|
| Q(workflow_job_template_node__workflow_job_template__in=wfjt_set)
|
||||||
| Q(team__in=team_set)
|
| Q(workflow_job__workflow_job_template__in=wfjt_set)
|
||||||
| Q(project__in=project_set)
|
)
|
||||||
| Q(project_update__project__in=project_set)
|
|
||||||
| Q(job_template__in=jt_set)
|
team_set = Team.accessible_pk_qs(self.user, 'read_role')
|
||||||
| Q(job__job_template__in=jt_set)
|
if team_set:
|
||||||
| Q(workflow_job_template__in=wfjt_set)
|
q |= Q(team__in=team_set)
|
||||||
| Q(workflow_job_template_node__workflow_job_template__in=wfjt_set)
|
|
||||||
| Q(workflow_job__workflow_job_template__in=wfjt_set)
|
app_set = OAuth2ApplicationAccess(self.user).filtered_queryset()
|
||||||
| Q(notification_template__organization__in=auditing_orgs)
|
if app_set:
|
||||||
| Q(notification__notification_template__organization__in=auditing_orgs)
|
q |= Q(o_auth2_application__in=app_set)
|
||||||
| Q(label__organization__in=auditing_orgs)
|
|
||||||
| Q(role__in=Role.objects.filter(ancestors__in=self.user.roles.all()) if auditing_orgs else [])
|
token_set = OAuth2TokenAccess(self.user).filtered_queryset()
|
||||||
).distinct()
|
if token_set:
|
||||||
|
q |= Q(o_auth2_access_token__in=token_set)
|
||||||
|
|
||||||
|
return qs.filter(q).distinct()
|
||||||
|
|
||||||
def can_add(self, data):
|
def can_add(self, data):
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -9,10 +9,16 @@ aim_inputs = {
|
|||||||
'fields': [
|
'fields': [
|
||||||
{
|
{
|
||||||
'id': 'url',
|
'id': 'url',
|
||||||
'label': _('CyberArk AIM URL'),
|
'label': _('CyberArk CCP URL'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'format': 'url',
|
'format': 'url',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'id': 'webservice_id',
|
||||||
|
'label': _('Web Service ID'),
|
||||||
|
'type': 'string',
|
||||||
|
'help_text': _('The CCP Web Service ID. Leave blank to default to AIMWebService.'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'id': 'app_id',
|
'id': 'app_id',
|
||||||
'label': _('Application ID'),
|
'label': _('Application ID'),
|
||||||
@@ -64,10 +70,13 @@ def aim_backend(**kwargs):
|
|||||||
client_cert = kwargs.get('client_cert', None)
|
client_cert = kwargs.get('client_cert', None)
|
||||||
client_key = kwargs.get('client_key', None)
|
client_key = kwargs.get('client_key', None)
|
||||||
verify = kwargs['verify']
|
verify = kwargs['verify']
|
||||||
|
webservice_id = kwargs['webservice_id']
|
||||||
app_id = kwargs['app_id']
|
app_id = kwargs['app_id']
|
||||||
object_query = kwargs['object_query']
|
object_query = kwargs['object_query']
|
||||||
object_query_format = kwargs['object_query_format']
|
object_query_format = kwargs['object_query_format']
|
||||||
reason = kwargs.get('reason', None)
|
reason = kwargs.get('reason', None)
|
||||||
|
if webservice_id == '':
|
||||||
|
webservice_id = 'AIMWebService'
|
||||||
|
|
||||||
query_params = {
|
query_params = {
|
||||||
'AppId': app_id,
|
'AppId': app_id,
|
||||||
@@ -78,7 +87,7 @@ def aim_backend(**kwargs):
|
|||||||
query_params['reason'] = reason
|
query_params['reason'] = reason
|
||||||
|
|
||||||
request_qs = '?' + urlencode(query_params, quote_via=quote)
|
request_qs = '?' + urlencode(query_params, quote_via=quote)
|
||||||
request_url = urljoin(url, '/'.join(['AIMWebService', 'api', 'Accounts']))
|
request_url = urljoin(url, '/'.join([webservice_id, 'api', 'Accounts']))
|
||||||
|
|
||||||
with CertFiles(client_cert, client_key) as cert:
|
with CertFiles(client_cert, client_key) as cert:
|
||||||
res = requests.get(
|
res = requests.get(
|
||||||
@@ -92,4 +101,4 @@ def aim_backend(**kwargs):
|
|||||||
return res.json()['Content']
|
return res.json()['Content']
|
||||||
|
|
||||||
|
|
||||||
aim_plugin = CredentialPlugin('CyberArk AIM Central Credential Provider Lookup', inputs=aim_inputs, backend=aim_backend)
|
aim_plugin = CredentialPlugin('CyberArk Central Credential Provider Lookup', inputs=aim_inputs, backend=aim_backend)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import copy
|
import copy
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import time
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
from .plugin import CredentialPlugin, CertFiles, raise_for_status
|
from .plugin import CredentialPlugin, CertFiles, raise_for_status
|
||||||
@@ -247,7 +248,15 @@ def kv_backend(**kwargs):
|
|||||||
request_url = urljoin(url, '/'.join(['v1'] + path_segments)).rstrip('/')
|
request_url = urljoin(url, '/'.join(['v1'] + path_segments)).rstrip('/')
|
||||||
with CertFiles(cacert) as cert:
|
with CertFiles(cacert) as cert:
|
||||||
request_kwargs['verify'] = cert
|
request_kwargs['verify'] = cert
|
||||||
response = sess.get(request_url, **request_kwargs)
|
request_retries = 0
|
||||||
|
while request_retries < 5:
|
||||||
|
response = sess.get(request_url, **request_kwargs)
|
||||||
|
# https://developer.hashicorp.com/vault/docs/enterprise/consistency
|
||||||
|
if response.status_code == 412:
|
||||||
|
request_retries += 1
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
break
|
||||||
raise_for_status(response)
|
raise_for_status(response)
|
||||||
|
|
||||||
json = response.json()
|
json = response.json()
|
||||||
@@ -289,8 +298,15 @@ def ssh_backend(**kwargs):
|
|||||||
|
|
||||||
with CertFiles(cacert) as cert:
|
with CertFiles(cacert) as cert:
|
||||||
request_kwargs['verify'] = cert
|
request_kwargs['verify'] = cert
|
||||||
resp = sess.post(request_url, **request_kwargs)
|
request_retries = 0
|
||||||
|
while request_retries < 5:
|
||||||
|
resp = sess.post(request_url, **request_kwargs)
|
||||||
|
# https://developer.hashicorp.com/vault/docs/enterprise/consistency
|
||||||
|
if resp.status_code == 412:
|
||||||
|
request_retries += 1
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
break
|
||||||
raise_for_status(resp)
|
raise_for_status(resp)
|
||||||
return resp.json()['data']['signed_key']
|
return resp.json()['data']['signed_key']
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
import traceback
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import now as tz_now
|
from django.utils.timezone import now as tz_now
|
||||||
from django.db import DatabaseError, OperationalError, transaction, connection as django_connection
|
from django.db import transaction, connection as django_connection
|
||||||
from django.db.utils import InterfaceError, InternalError
|
|
||||||
from django_guid import set_guid
|
from django_guid import set_guid
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
@@ -64,6 +62,7 @@ class CallbackBrokerWorker(BaseWorker):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
MAX_RETRIES = 2
|
MAX_RETRIES = 2
|
||||||
|
INDIVIDUAL_EVENT_RETRIES = 3
|
||||||
last_stats = time.time()
|
last_stats = time.time()
|
||||||
last_flush = time.time()
|
last_flush = time.time()
|
||||||
total = 0
|
total = 0
|
||||||
@@ -164,38 +163,48 @@ class CallbackBrokerWorker(BaseWorker):
|
|||||||
else: # only calculate the seconds if the created time already has been set
|
else: # only calculate the seconds if the created time already has been set
|
||||||
metrics_total_job_event_processing_seconds += e.modified - e.created
|
metrics_total_job_event_processing_seconds += e.modified - e.created
|
||||||
metrics_duration_to_save = time.perf_counter()
|
metrics_duration_to_save = time.perf_counter()
|
||||||
|
saved_events = []
|
||||||
try:
|
try:
|
||||||
cls.objects.bulk_create(events)
|
cls.objects.bulk_create(events)
|
||||||
metrics_bulk_events_saved += len(events)
|
metrics_bulk_events_saved += len(events)
|
||||||
|
saved_events = events
|
||||||
|
self.buff[cls] = []
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(f'Error in events bulk_create, will try indiviually up to 5 errors, error {str(exc)}')
|
# If the database is flaking, let ensure_connection throw a general exception
|
||||||
|
# will be caught by the outer loop, which goes into a proper sleep and retry loop
|
||||||
|
django_connection.ensure_connection()
|
||||||
|
logger.warning(f'Error in events bulk_create, will try indiviually, error: {str(exc)}')
|
||||||
# if an exception occurs, we should re-attempt to save the
|
# if an exception occurs, we should re-attempt to save the
|
||||||
# events one-by-one, because something in the list is
|
# events one-by-one, because something in the list is
|
||||||
# broken/stale
|
# broken/stale
|
||||||
consecutive_errors = 0
|
|
||||||
events_saved = 0
|
|
||||||
metrics_events_batch_save_errors += 1
|
metrics_events_batch_save_errors += 1
|
||||||
for e in events:
|
for e in events.copy():
|
||||||
try:
|
try:
|
||||||
e.save()
|
e.save()
|
||||||
events_saved += 1
|
metrics_singular_events_saved += 1
|
||||||
consecutive_errors = 0
|
events.remove(e)
|
||||||
|
saved_events.append(e) # Importantly, remove successfully saved events from the buffer
|
||||||
except Exception as exc_indv:
|
except Exception as exc_indv:
|
||||||
consecutive_errors += 1
|
retry_count = getattr(e, '_retry_count', 0) + 1
|
||||||
logger.info(f'Database Error Saving individual Job Event, error {str(exc_indv)}')
|
e._retry_count = retry_count
|
||||||
if consecutive_errors >= 5:
|
|
||||||
raise
|
# special sanitization logic for postgres treatment of NUL 0x00 char
|
||||||
metrics_singular_events_saved += events_saved
|
if (retry_count == 1) and isinstance(exc_indv, ValueError) and ("\x00" in e.stdout):
|
||||||
if events_saved == 0:
|
e.stdout = e.stdout.replace("\x00", "")
|
||||||
raise
|
|
||||||
|
if retry_count >= self.INDIVIDUAL_EVENT_RETRIES:
|
||||||
|
logger.error(f'Hit max retries ({retry_count}) saving individual Event error: {str(exc_indv)}\ndata:\n{e.__dict__}')
|
||||||
|
events.remove(e)
|
||||||
|
else:
|
||||||
|
logger.info(f'Database Error Saving individual Event uuid={e.uuid} try={retry_count}, error: {str(exc_indv)}')
|
||||||
|
|
||||||
metrics_duration_to_save = time.perf_counter() - metrics_duration_to_save
|
metrics_duration_to_save = time.perf_counter() - metrics_duration_to_save
|
||||||
for e in events:
|
for e in saved_events:
|
||||||
if not getattr(e, '_skip_websocket_message', False):
|
if not getattr(e, '_skip_websocket_message', False):
|
||||||
metrics_events_broadcast += 1
|
metrics_events_broadcast += 1
|
||||||
emit_event_detail(e)
|
emit_event_detail(e)
|
||||||
if getattr(e, '_notification_trigger_event', False):
|
if getattr(e, '_notification_trigger_event', False):
|
||||||
job_stats_wrapup(getattr(e, e.JOB_REFERENCE), event=e)
|
job_stats_wrapup(getattr(e, e.JOB_REFERENCE), event=e)
|
||||||
self.buff = {}
|
|
||||||
self.last_flush = time.time()
|
self.last_flush = time.time()
|
||||||
# only update metrics if we saved events
|
# only update metrics if we saved events
|
||||||
if (metrics_bulk_events_saved + metrics_singular_events_saved) > 0:
|
if (metrics_bulk_events_saved + metrics_singular_events_saved) > 0:
|
||||||
@@ -267,20 +276,16 @@ class CallbackBrokerWorker(BaseWorker):
|
|||||||
try:
|
try:
|
||||||
self.flush(force=flush)
|
self.flush(force=flush)
|
||||||
break
|
break
|
||||||
except (OperationalError, InterfaceError, InternalError) as exc:
|
except Exception as exc:
|
||||||
|
# Aside form bugs, exceptions here are assumed to be due to database flake
|
||||||
if retries >= self.MAX_RETRIES:
|
if retries >= self.MAX_RETRIES:
|
||||||
logger.exception('Worker could not re-establish database connectivity, giving up on one or more events.')
|
logger.exception('Worker could not re-establish database connectivity, giving up on one or more events.')
|
||||||
|
self.buff = {}
|
||||||
return
|
return
|
||||||
delay = 60 * retries
|
delay = 60 * retries
|
||||||
logger.warning(f'Database Error Flushing Job Events, retry #{retries + 1} in {delay} seconds: {str(exc)}')
|
logger.warning(f'Database Error Flushing Job Events, retry #{retries + 1} in {delay} seconds: {str(exc)}')
|
||||||
django_connection.close()
|
django_connection.close()
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
retries += 1
|
retries += 1
|
||||||
except DatabaseError:
|
except Exception:
|
||||||
logger.exception('Database Error Flushing Job Events')
|
logger.exception(f'Callback Task Processor Raised Unexpected Exception processing event data:\n{body}')
|
||||||
django_connection.close()
|
|
||||||
break
|
|
||||||
except Exception as exc:
|
|
||||||
tb = traceback.format_exc()
|
|
||||||
logger.error('Callback Task Processor Raised Exception: %r', exc)
|
|
||||||
logger.error('Detail: {}'.format(tb))
|
|
||||||
|
|||||||
@@ -32,8 +32,14 @@ class Command(BaseCommand):
|
|||||||
def handle(self, **options):
|
def handle(self, **options):
|
||||||
self.old_key = settings.SECRET_KEY
|
self.old_key = settings.SECRET_KEY
|
||||||
custom_key = os.environ.get("TOWER_SECRET_KEY")
|
custom_key = os.environ.get("TOWER_SECRET_KEY")
|
||||||
if options.get("use_custom_key") and custom_key:
|
if options.get("use_custom_key"):
|
||||||
self.new_key = custom_key
|
if custom_key:
|
||||||
|
self.new_key = custom_key
|
||||||
|
else:
|
||||||
|
print("Use custom key was specified but the env var TOWER_SECRET_KEY was not available")
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
self.new_key = base64.encodebytes(os.urandom(33)).decode().rstrip()
|
self.new_key = base64.encodebytes(os.urandom(33)).decode().rstrip()
|
||||||
self._notification_templates()
|
self._notification_templates()
|
||||||
|
|||||||
@@ -158,7 +158,11 @@ class InstanceManager(models.Manager):
|
|||||||
return (False, instance)
|
return (False, instance)
|
||||||
|
|
||||||
# Create new instance, and fill in default values
|
# Create new instance, and fill in default values
|
||||||
create_defaults = {'node_state': Instance.States.INSTALLED, 'capacity': 0}
|
create_defaults = {
|
||||||
|
'node_state': Instance.States.INSTALLED,
|
||||||
|
'capacity': 0,
|
||||||
|
'listener_port': 27199,
|
||||||
|
}
|
||||||
if defaults is not None:
|
if defaults is not None:
|
||||||
create_defaults.update(defaults)
|
create_defaults.update(defaults)
|
||||||
uuid_option = {}
|
uuid_option = {}
|
||||||
|
|||||||
18
awx/main/migrations/0174_ensure_org_ee_admin_roles.py
Normal file
18
awx/main/migrations/0174_ensure_org_ee_admin_roles.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.16 on 2022-12-07 21:11
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from awx.main.migrations import _rbac as rbac
|
||||||
|
from awx.main.migrations import _migration_utils as migration_utils
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0173_instancegroup_max_limits'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
|
||||||
|
migrations.RunPython(rbac.create_roles),
|
||||||
|
]
|
||||||
@@ -15,6 +15,7 @@ def aws(cred, env, private_data_dir):
|
|||||||
|
|
||||||
if cred.has_input('security_token'):
|
if cred.has_input('security_token'):
|
||||||
env['AWS_SECURITY_TOKEN'] = cred.get_input('security_token', default='')
|
env['AWS_SECURITY_TOKEN'] = cred.get_input('security_token', default='')
|
||||||
|
env['AWS_SESSION_TOKEN'] = env['AWS_SECURITY_TOKEN']
|
||||||
|
|
||||||
|
|
||||||
def gce(cred, env, private_data_dir):
|
def gce(cred, env, private_data_dir):
|
||||||
|
|||||||
@@ -507,7 +507,7 @@ class TaskManager(TaskBase):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@timeit
|
@timeit
|
||||||
def start_task(self, task, instance_group, dependent_tasks=None, instance=None):
|
def start_task(self, task, instance_group, instance=None):
|
||||||
# Just like for process_running_tasks, add the job to the dependency graph and
|
# Just like for process_running_tasks, add the job to the dependency graph and
|
||||||
# ask the TaskManagerInstanceGroups object to update consumed capacity on all
|
# ask the TaskManagerInstanceGroups object to update consumed capacity on all
|
||||||
# implicated instances and container groups.
|
# implicated instances and container groups.
|
||||||
@@ -524,14 +524,6 @@ class TaskManager(TaskBase):
|
|||||||
ScheduleTaskManager().schedule()
|
ScheduleTaskManager().schedule()
|
||||||
from awx.main.tasks.system import handle_work_error, handle_work_success
|
from awx.main.tasks.system import handle_work_error, handle_work_success
|
||||||
|
|
||||||
dependent_tasks = dependent_tasks or []
|
|
||||||
|
|
||||||
task_actual = {
|
|
||||||
'type': get_type_for_model(type(task)),
|
|
||||||
'id': task.id,
|
|
||||||
}
|
|
||||||
dependencies = [{'type': get_type_for_model(type(t)), 'id': t.id} for t in dependent_tasks]
|
|
||||||
|
|
||||||
task.status = 'waiting'
|
task.status = 'waiting'
|
||||||
|
|
||||||
(start_status, opts) = task.pre_start()
|
(start_status, opts) = task.pre_start()
|
||||||
@@ -563,6 +555,7 @@ class TaskManager(TaskBase):
|
|||||||
# apply_async does a NOTIFY to the channel dispatcher is listening to
|
# apply_async does a NOTIFY to the channel dispatcher is listening to
|
||||||
# postgres will treat this as part of the transaction, which is what we want
|
# postgres will treat this as part of the transaction, which is what we want
|
||||||
if task.status != 'failed' and type(task) is not WorkflowJob:
|
if task.status != 'failed' and type(task) is not WorkflowJob:
|
||||||
|
task_actual = {'type': get_type_for_model(type(task)), 'id': task.id}
|
||||||
task_cls = task._get_task_class()
|
task_cls = task._get_task_class()
|
||||||
task_cls.apply_async(
|
task_cls.apply_async(
|
||||||
[task.pk],
|
[task.pk],
|
||||||
@@ -570,7 +563,7 @@ class TaskManager(TaskBase):
|
|||||||
queue=task.get_queue_name(),
|
queue=task.get_queue_name(),
|
||||||
uuid=task.celery_task_id,
|
uuid=task.celery_task_id,
|
||||||
callbacks=[{'task': handle_work_success.name, 'kwargs': {'task_actual': task_actual}}],
|
callbacks=[{'task': handle_work_success.name, 'kwargs': {'task_actual': task_actual}}],
|
||||||
errbacks=[{'task': handle_work_error.name, 'args': [task.celery_task_id], 'kwargs': {'subtasks': [task_actual] + dependencies}}],
|
errbacks=[{'task': handle_work_error.name, 'kwargs': {'task_actual': task_actual}}],
|
||||||
)
|
)
|
||||||
|
|
||||||
# In exception cases, like a job failing pre-start checks, we send the websocket status message
|
# In exception cases, like a job failing pre-start checks, we send the websocket status message
|
||||||
@@ -609,7 +602,7 @@ class TaskManager(TaskBase):
|
|||||||
if isinstance(task, WorkflowJob):
|
if isinstance(task, WorkflowJob):
|
||||||
# Previously we were tracking allow_simultaneous blocking both here and in DependencyGraph.
|
# Previously we were tracking allow_simultaneous blocking both here and in DependencyGraph.
|
||||||
# Double check that using just the DependencyGraph works for Workflows and Sliced Jobs.
|
# Double check that using just the DependencyGraph works for Workflows and Sliced Jobs.
|
||||||
self.start_task(task, None, task.get_jobs_fail_chain(), None)
|
self.start_task(task, None, None)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
found_acceptable_queue = False
|
found_acceptable_queue = False
|
||||||
@@ -637,7 +630,7 @@ class TaskManager(TaskBase):
|
|||||||
execution_instance = self.tm_models.instances[control_instance.hostname].obj
|
execution_instance = self.tm_models.instances[control_instance.hostname].obj
|
||||||
task.log_lifecycle("controller_node_chosen")
|
task.log_lifecycle("controller_node_chosen")
|
||||||
task.log_lifecycle("execution_node_chosen")
|
task.log_lifecycle("execution_node_chosen")
|
||||||
self.start_task(task, self.controlplane_ig, task.get_jobs_fail_chain(), execution_instance)
|
self.start_task(task, self.controlplane_ig, execution_instance)
|
||||||
found_acceptable_queue = True
|
found_acceptable_queue = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -645,7 +638,7 @@ class TaskManager(TaskBase):
|
|||||||
if not self.tm_models.instance_groups[instance_group.name].has_remaining_capacity(task):
|
if not self.tm_models.instance_groups[instance_group.name].has_remaining_capacity(task):
|
||||||
continue
|
continue
|
||||||
if instance_group.is_container_group:
|
if instance_group.is_container_group:
|
||||||
self.start_task(task, instance_group, task.get_jobs_fail_chain(), None)
|
self.start_task(task, instance_group, None)
|
||||||
found_acceptable_queue = True
|
found_acceptable_queue = True
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -670,7 +663,7 @@ class TaskManager(TaskBase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
execution_instance = self.tm_models.instances[execution_instance.hostname].obj
|
execution_instance = self.tm_models.instances[execution_instance.hostname].obj
|
||||||
self.start_task(task, instance_group, task.get_jobs_fail_chain(), execution_instance)
|
self.start_task(task, instance_group, execution_instance)
|
||||||
found_acceptable_queue = True
|
found_acceptable_queue = True
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -390,6 +390,7 @@ class BaseTask(object):
|
|||||||
logger.error("I/O error({0}) while trying to open lock file [{1}]: {2}".format(e.errno, lock_path, e.strerror))
|
logger.error("I/O error({0}) while trying to open lock file [{1}]: {2}".format(e.errno, lock_path, e.strerror))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
emitted_lockfile_log = False
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -401,6 +402,9 @@ class BaseTask(object):
|
|||||||
logger.error("I/O error({0}) while trying to aquire lock on file [{1}]: {2}".format(e.errno, lock_path, e.strerror))
|
logger.error("I/O error({0}) while trying to aquire lock on file [{1}]: {2}".format(e.errno, lock_path, e.strerror))
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
|
if not emitted_lockfile_log:
|
||||||
|
logger.info(f"exception acquiring lock {lock_path}: {e}")
|
||||||
|
emitted_lockfile_log = True
|
||||||
time.sleep(1.0)
|
time.sleep(1.0)
|
||||||
self.instance.refresh_from_db(fields=['cancel_flag'])
|
self.instance.refresh_from_db(fields=['cancel_flag'])
|
||||||
if self.instance.cancel_flag or signal_callback():
|
if self.instance.cancel_flag or signal_callback():
|
||||||
|
|||||||
@@ -411,9 +411,11 @@ class AWXReceptorJob:
|
|||||||
unit_status = receptor_ctl.simple_command(f'work status {self.unit_id}')
|
unit_status = receptor_ctl.simple_command(f'work status {self.unit_id}')
|
||||||
detail = unit_status.get('Detail', None)
|
detail = unit_status.get('Detail', None)
|
||||||
state_name = unit_status.get('StateName', None)
|
state_name = unit_status.get('StateName', None)
|
||||||
|
stdout_size = unit_status.get('StdoutSize', 0)
|
||||||
except Exception:
|
except Exception:
|
||||||
detail = ''
|
detail = ''
|
||||||
state_name = ''
|
state_name = ''
|
||||||
|
stdout_size = 0
|
||||||
logger.exception(f'An error was encountered while getting status for work unit {self.unit_id}')
|
logger.exception(f'An error was encountered while getting status for work unit {self.unit_id}')
|
||||||
|
|
||||||
if 'exceeded quota' in detail:
|
if 'exceeded quota' in detail:
|
||||||
@@ -424,9 +426,16 @@ class AWXReceptorJob:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resultsock = receptor_ctl.get_work_results(self.unit_id, return_sockfile=True)
|
receptor_output = ''
|
||||||
lines = resultsock.readlines()
|
if state_name == 'Failed' and self.task.runner_callback.event_ct == 0:
|
||||||
receptor_output = b"".join(lines).decode()
|
# if receptor work unit failed and no events were emitted, work results may
|
||||||
|
# contain useful information about why the job failed. In case stdout is
|
||||||
|
# massive, only ask for last 1000 bytes
|
||||||
|
startpos = max(stdout_size - 1000, 0)
|
||||||
|
resultsock, resultfile = receptor_ctl.get_work_results(self.unit_id, startpos=startpos, return_socket=True, return_sockfile=True)
|
||||||
|
resultsock.setblocking(False) # this makes resultfile reads non blocking
|
||||||
|
lines = resultfile.readlines()
|
||||||
|
receptor_output = b"".join(lines).decode()
|
||||||
if receptor_output:
|
if receptor_output:
|
||||||
self.task.runner_callback.delay_update(result_traceback=receptor_output)
|
self.task.runner_callback.delay_update(result_traceback=receptor_output)
|
||||||
elif detail:
|
elif detail:
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ from awx.main.constants import ACTIVE_STATES
|
|||||||
from awx.main.dispatch.publish import task
|
from awx.main.dispatch.publish import task
|
||||||
from awx.main.dispatch import get_local_queuename, reaper
|
from awx.main.dispatch import get_local_queuename, reaper
|
||||||
from awx.main.utils.common import (
|
from awx.main.utils.common import (
|
||||||
|
get_type_for_model,
|
||||||
ignore_inventory_computed_fields,
|
ignore_inventory_computed_fields,
|
||||||
ignore_inventory_group_removal,
|
ignore_inventory_group_removal,
|
||||||
ScheduleWorkflowManager,
|
ScheduleWorkflowManager,
|
||||||
@@ -720,45 +721,43 @@ def handle_work_success(task_actual):
|
|||||||
|
|
||||||
|
|
||||||
@task(queue=get_local_queuename)
|
@task(queue=get_local_queuename)
|
||||||
def handle_work_error(task_id, *args, **kwargs):
|
def handle_work_error(task_actual):
|
||||||
subtasks = kwargs.get('subtasks', None)
|
try:
|
||||||
logger.debug('Executing error task id %s, subtasks: %s' % (task_id, str(subtasks)))
|
instance = UnifiedJob.get_instance_by_type(task_actual['type'], task_actual['id'])
|
||||||
first_instance = None
|
except ObjectDoesNotExist:
|
||||||
first_instance_type = ''
|
logger.warning('Missing {} `{}` in error callback.'.format(task_actual['type'], task_actual['id']))
|
||||||
if subtasks is not None:
|
return
|
||||||
for each_task in subtasks:
|
if not instance:
|
||||||
try:
|
return
|
||||||
instance = UnifiedJob.get_instance_by_type(each_task['type'], each_task['id'])
|
|
||||||
if not instance:
|
|
||||||
# Unknown task type
|
|
||||||
logger.warning("Unknown task type: {}".format(each_task['type']))
|
|
||||||
continue
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
logger.warning('Missing {} `{}` in error callback.'.format(each_task['type'], each_task['id']))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if first_instance is None:
|
subtasks = instance.get_jobs_fail_chain() # reverse of dependent_jobs mostly
|
||||||
first_instance = instance
|
logger.debug(f'Executing error task id {task_actual["id"]}, subtasks: {[subtask.id for subtask in subtasks]}')
|
||||||
first_instance_type = each_task['type']
|
|
||||||
|
|
||||||
if instance.celery_task_id != task_id and not instance.cancel_flag and not instance.status in ('successful', 'failed'):
|
deps_of_deps = {}
|
||||||
instance.status = 'failed'
|
|
||||||
instance.failed = True
|
for subtask in subtasks:
|
||||||
if not instance.job_explanation:
|
if subtask.celery_task_id != instance.celery_task_id and not subtask.cancel_flag and not subtask.status in ('successful', 'failed'):
|
||||||
instance.job_explanation = 'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % (
|
# If there are multiple in the dependency chain, A->B->C, and this was called for A, blame B for clarity
|
||||||
first_instance_type,
|
blame_job = deps_of_deps.get(subtask.id, instance)
|
||||||
first_instance.name,
|
subtask.status = 'failed'
|
||||||
first_instance.id,
|
subtask.failed = True
|
||||||
)
|
if not subtask.job_explanation:
|
||||||
instance.save()
|
subtask.job_explanation = 'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % (
|
||||||
instance.websocket_emit_status("failed")
|
get_type_for_model(type(blame_job)),
|
||||||
|
blame_job.name,
|
||||||
|
blame_job.id,
|
||||||
|
)
|
||||||
|
subtask.save()
|
||||||
|
subtask.websocket_emit_status("failed")
|
||||||
|
|
||||||
|
for sub_subtask in subtask.get_jobs_fail_chain():
|
||||||
|
deps_of_deps[sub_subtask.id] = subtask
|
||||||
|
|
||||||
# We only send 1 job complete message since all the job completion message
|
# We only send 1 job complete message since all the job completion message
|
||||||
# handling does is trigger the scheduler. If we extend the functionality of
|
# handling does is trigger the scheduler. If we extend the functionality of
|
||||||
# what the job complete message handler does then we may want to send a
|
# what the job complete message handler does then we may want to send a
|
||||||
# completion event for each job here.
|
# completion event for each job here.
|
||||||
if first_instance:
|
schedule_manager_success_or_error(instance)
|
||||||
schedule_manager_success_or_error(first_instance)
|
|
||||||
|
|
||||||
|
|
||||||
@task(queue=get_local_queuename)
|
@task(queue=get_local_queuename)
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||||
"AWS_ACCESS_KEY_ID": "fooo",
|
"AWS_ACCESS_KEY_ID": "fooo",
|
||||||
"AWS_SECRET_ACCESS_KEY": "fooo",
|
"AWS_SECRET_ACCESS_KEY": "fooo",
|
||||||
"AWS_SECURITY_TOKEN": "fooo"
|
"AWS_SECURITY_TOKEN": "fooo",
|
||||||
|
"AWS_SESSION_TOKEN": "fooo"
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
import time
|
||||||
|
from unittest import mock
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
|
from awx.main.dispatch.worker.callback import job_stats_wrapup, CallbackBrokerWorker
|
||||||
|
|
||||||
from awx.main.dispatch.worker.callback import job_stats_wrapup
|
|
||||||
from awx.main.models.jobs import Job
|
from awx.main.models.jobs import Job
|
||||||
|
from awx.main.models.inventory import InventoryUpdate, InventorySource
|
||||||
|
from awx.main.models.events import InventoryUpdateEvent
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -24,3 +32,108 @@ def test_wrapup_does_send_notifications(mocker):
|
|||||||
job.refresh_from_db()
|
job.refresh_from_db()
|
||||||
assert job.host_status_counts == {}
|
assert job.host_status_counts == {}
|
||||||
mock.assert_called_once_with('succeeded')
|
mock.assert_called_once_with('succeeded')
|
||||||
|
|
||||||
|
|
||||||
|
class FakeRedis:
|
||||||
|
def keys(self, *args, **kwargs):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def set(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_url(cls, *args, **kwargs):
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
def pipeline(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class TestCallbackBrokerWorker(TransactionTestCase):
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def turn_off_websockets(self):
|
||||||
|
with mock.patch('awx.main.dispatch.worker.callback.emit_event_detail', lambda *a, **kw: None):
|
||||||
|
yield
|
||||||
|
|
||||||
|
def get_worker(self):
|
||||||
|
with mock.patch('redis.Redis', new=FakeRedis): # turn off redis stuff
|
||||||
|
return CallbackBrokerWorker()
|
||||||
|
|
||||||
|
def event_create_kwargs(self):
|
||||||
|
inventory_update = InventoryUpdate.objects.create(source='file', inventory_source=InventorySource.objects.create(source='file'))
|
||||||
|
return dict(inventory_update=inventory_update, created=inventory_update.created)
|
||||||
|
|
||||||
|
def test_flush_with_valid_event(self):
|
||||||
|
worker = self.get_worker()
|
||||||
|
events = [InventoryUpdateEvent(uuid=str(uuid4()), **self.event_create_kwargs())]
|
||||||
|
worker.buff = {InventoryUpdateEvent: events}
|
||||||
|
worker.flush()
|
||||||
|
assert worker.buff.get(InventoryUpdateEvent, []) == []
|
||||||
|
assert InventoryUpdateEvent.objects.filter(uuid=events[0].uuid).count() == 1
|
||||||
|
|
||||||
|
def test_flush_with_invalid_event(self):
|
||||||
|
worker = self.get_worker()
|
||||||
|
kwargs = self.event_create_kwargs()
|
||||||
|
events = [
|
||||||
|
InventoryUpdateEvent(uuid=str(uuid4()), stdout='good1', **kwargs),
|
||||||
|
InventoryUpdateEvent(uuid=str(uuid4()), stdout='bad', counter=-2, **kwargs),
|
||||||
|
InventoryUpdateEvent(uuid=str(uuid4()), stdout='good2', **kwargs),
|
||||||
|
]
|
||||||
|
worker.buff = {InventoryUpdateEvent: events.copy()}
|
||||||
|
worker.flush()
|
||||||
|
assert InventoryUpdateEvent.objects.filter(uuid=events[0].uuid).count() == 1
|
||||||
|
assert InventoryUpdateEvent.objects.filter(uuid=events[1].uuid).count() == 0
|
||||||
|
assert InventoryUpdateEvent.objects.filter(uuid=events[2].uuid).count() == 1
|
||||||
|
assert worker.buff == {InventoryUpdateEvent: [events[1]]}
|
||||||
|
|
||||||
|
def test_duplicate_key_not_saved_twice(self):
|
||||||
|
worker = self.get_worker()
|
||||||
|
events = [InventoryUpdateEvent(uuid=str(uuid4()), **self.event_create_kwargs())]
|
||||||
|
worker.buff = {InventoryUpdateEvent: events.copy()}
|
||||||
|
worker.flush()
|
||||||
|
|
||||||
|
# put current saved event in buffer (error case)
|
||||||
|
worker.buff = {InventoryUpdateEvent: [InventoryUpdateEvent.objects.get(uuid=events[0].uuid)]}
|
||||||
|
worker.last_flush = time.time() - 2.0
|
||||||
|
# here, the bulk_create will fail with UNIQUE constraint violation, but individual saves should resolve it
|
||||||
|
worker.flush()
|
||||||
|
assert InventoryUpdateEvent.objects.filter(uuid=events[0].uuid).count() == 1
|
||||||
|
assert worker.buff.get(InventoryUpdateEvent, []) == []
|
||||||
|
|
||||||
|
def test_give_up_on_bad_event(self):
|
||||||
|
worker = self.get_worker()
|
||||||
|
events = [InventoryUpdateEvent(uuid=str(uuid4()), counter=-2, **self.event_create_kwargs())]
|
||||||
|
worker.buff = {InventoryUpdateEvent: events.copy()}
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
worker.last_flush = time.time() - 2.0
|
||||||
|
worker.flush()
|
||||||
|
|
||||||
|
# Could not save, should be logged, and buffer should be cleared
|
||||||
|
assert worker.buff.get(InventoryUpdateEvent, []) == []
|
||||||
|
assert InventoryUpdateEvent.objects.filter(uuid=events[0].uuid).count() == 0 # sanity
|
||||||
|
|
||||||
|
def test_postgres_invalid_NUL_char(self):
|
||||||
|
# In postgres, text fields reject NUL character, 0x00
|
||||||
|
# tests use sqlite3 which will not raise an error
|
||||||
|
# but we can still test that it is sanitized before saving
|
||||||
|
worker = self.get_worker()
|
||||||
|
kwargs = self.event_create_kwargs()
|
||||||
|
events = [InventoryUpdateEvent(uuid=str(uuid4()), stdout="\x00", **kwargs)]
|
||||||
|
assert "\x00" in events[0].stdout # sanity
|
||||||
|
worker.buff = {InventoryUpdateEvent: events.copy()}
|
||||||
|
|
||||||
|
with mock.patch.object(InventoryUpdateEvent.objects, 'bulk_create', side_effect=ValueError):
|
||||||
|
with mock.patch.object(events[0], 'save', side_effect=ValueError):
|
||||||
|
worker.flush()
|
||||||
|
|
||||||
|
assert "\x00" not in events[0].stdout
|
||||||
|
|
||||||
|
worker.last_flush = time.time() - 2.0
|
||||||
|
worker.flush()
|
||||||
|
|
||||||
|
event = InventoryUpdateEvent.objects.get(uuid=events[0].uuid)
|
||||||
|
assert "\x00" not in event.stdout
|
||||||
|
|||||||
@@ -171,13 +171,17 @@ class TestKeyRegeneration:
|
|||||||
|
|
||||||
def test_use_custom_key_with_empty_tower_secret_key_env_var(self):
|
def test_use_custom_key_with_empty_tower_secret_key_env_var(self):
|
||||||
os.environ['TOWER_SECRET_KEY'] = ''
|
os.environ['TOWER_SECRET_KEY'] = ''
|
||||||
new_key = call_command('regenerate_secret_key', '--use-custom-key')
|
with pytest.raises(SystemExit) as e:
|
||||||
assert settings.SECRET_KEY != new_key
|
call_command('regenerate_secret_key', '--use-custom-key')
|
||||||
|
assert e.type == SystemExit
|
||||||
|
assert e.value.code == 1
|
||||||
|
|
||||||
def test_use_custom_key_with_no_tower_secret_key_env_var(self):
|
def test_use_custom_key_with_no_tower_secret_key_env_var(self):
|
||||||
os.environ.pop('TOWER_SECRET_KEY', None)
|
os.environ.pop('TOWER_SECRET_KEY', None)
|
||||||
new_key = call_command('regenerate_secret_key', '--use-custom-key')
|
with pytest.raises(SystemExit) as e:
|
||||||
assert settings.SECRET_KEY != new_key
|
call_command('regenerate_secret_key', '--use-custom-key')
|
||||||
|
assert e.type == SystemExit
|
||||||
|
assert e.value.code == 1
|
||||||
|
|
||||||
def test_with_tower_secret_key_env_var(self):
|
def test_with_tower_secret_key_env_var(self):
|
||||||
custom_key = 'MXSq9uqcwezBOChl/UfmbW1k4op+bC+FQtwPqgJ1u9XV'
|
custom_key = 'MXSq9uqcwezBOChl/UfmbW1k4op+bC+FQtwPqgJ1u9XV'
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ def test_multi_group_basic_job_launch(instance_factory, controlplane_instance_gr
|
|||||||
mock_task_impact.return_value = 500
|
mock_task_impact.return_value = 500
|
||||||
with mocker.patch("awx.main.scheduler.TaskManager.start_task"):
|
with mocker.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_has_calls([mock.call(j1, ig1, [], i1), mock.call(j2, ig2, [], i2)])
|
TaskManager.start_task.assert_has_calls([mock.call(j1, ig1, i1), mock.call(j2, ig2, i2)])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -54,7 +54,7 @@ def test_multi_group_with_shared_dependency(instance_factory, controlplane_insta
|
|||||||
DependencyManager().schedule()
|
DependencyManager().schedule()
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
pu = p.project_updates.first()
|
pu = p.project_updates.first()
|
||||||
TaskManager.start_task.assert_called_once_with(pu, controlplane_instance_group, [j1, j2], controlplane_instance_group.instances.all()[0])
|
TaskManager.start_task.assert_called_once_with(pu, controlplane_instance_group, controlplane_instance_group.instances.all()[0])
|
||||||
pu.finished = pu.created + timedelta(seconds=1)
|
pu.finished = pu.created + timedelta(seconds=1)
|
||||||
pu.status = "successful"
|
pu.status = "successful"
|
||||||
pu.save()
|
pu.save()
|
||||||
@@ -62,8 +62,8 @@ def test_multi_group_with_shared_dependency(instance_factory, controlplane_insta
|
|||||||
DependencyManager().schedule()
|
DependencyManager().schedule()
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
|
|
||||||
TaskManager.start_task.assert_any_call(j1, ig1, [], i1)
|
TaskManager.start_task.assert_any_call(j1, ig1, i1)
|
||||||
TaskManager.start_task.assert_any_call(j2, ig2, [], i2)
|
TaskManager.start_task.assert_any_call(j2, ig2, i2)
|
||||||
assert TaskManager.start_task.call_count == 2
|
assert TaskManager.start_task.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ def test_workflow_job_no_instancegroup(workflow_job_template_factory, controlpla
|
|||||||
wfj.save()
|
wfj.save()
|
||||||
with mocker.patch("awx.main.scheduler.TaskManager.start_task"):
|
with mocker.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(wfj, None, [], None)
|
TaskManager.start_task.assert_called_once_with(wfj, None, None)
|
||||||
assert wfj.instance_group is None
|
assert wfj.instance_group is None
|
||||||
|
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ def test_failover_group_run(instance_factory, controlplane_instance_group, mocke
|
|||||||
mock_task_impact.return_value = 500
|
mock_task_impact.return_value = 500
|
||||||
with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job:
|
with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job:
|
||||||
tm.schedule()
|
tm.schedule()
|
||||||
mock_job.assert_has_calls([mock.call(j1, ig1, [], i1), mock.call(j1_1, ig2, [], i2)])
|
mock_job.assert_has_calls([mock.call(j1, ig1, i1), mock.call(j1_1, ig2, i2)])
|
||||||
assert mock_job.call_count == 2
|
assert mock_job.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def test_single_job_scheduler_launch(hybrid_instance, controlplane_instance_grou
|
|||||||
j = create_job(objects.job_template)
|
j = create_job(objects.job_template)
|
||||||
with mocker.patch("awx.main.scheduler.TaskManager.start_task"):
|
with mocker.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, [], instance)
|
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, instance)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -240,12 +240,12 @@ def test_multi_jt_capacity_blocking(hybrid_instance, job_template_factory, mocke
|
|||||||
mock_task_impact.return_value = 505
|
mock_task_impact.return_value = 505
|
||||||
with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job:
|
with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job:
|
||||||
tm.schedule()
|
tm.schedule()
|
||||||
mock_job.assert_called_once_with(j1, controlplane_instance_group, [], instance)
|
mock_job.assert_called_once_with(j1, controlplane_instance_group, instance)
|
||||||
j1.status = "successful"
|
j1.status = "successful"
|
||||||
j1.save()
|
j1.save()
|
||||||
with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job:
|
with mock.patch.object(TaskManager, "start_task", wraps=tm.start_task) as mock_job:
|
||||||
tm.schedule()
|
tm.schedule()
|
||||||
mock_job.assert_called_once_with(j2, controlplane_instance_group, [], instance)
|
mock_job.assert_called_once_with(j2, controlplane_instance_group, instance)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -337,12 +337,12 @@ def test_single_job_dependencies_project_launch(controlplane_instance_group, job
|
|||||||
pu = [x for x in p.project_updates.all()]
|
pu = [x for x in p.project_updates.all()]
|
||||||
assert len(pu) == 1
|
assert len(pu) == 1
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(pu[0], controlplane_instance_group, [j], instance)
|
TaskManager.start_task.assert_called_once_with(pu[0], controlplane_instance_group, instance)
|
||||||
pu[0].status = "successful"
|
pu[0].status = "successful"
|
||||||
pu[0].save()
|
pu[0].save()
|
||||||
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, [], instance)
|
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, instance)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -365,12 +365,12 @@ def test_single_job_dependencies_inventory_update_launch(controlplane_instance_g
|
|||||||
iu = [x for x in ii.inventory_updates.all()]
|
iu = [x for x in ii.inventory_updates.all()]
|
||||||
assert len(iu) == 1
|
assert len(iu) == 1
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(iu[0], controlplane_instance_group, [j], instance)
|
TaskManager.start_task.assert_called_once_with(iu[0], controlplane_instance_group, instance)
|
||||||
iu[0].status = "successful"
|
iu[0].status = "successful"
|
||||||
iu[0].save()
|
iu[0].save()
|
||||||
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, [], instance)
|
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, instance)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -412,7 +412,7 @@ def test_job_dependency_with_already_updated(controlplane_instance_group, job_te
|
|||||||
mock_iu.assert_not_called()
|
mock_iu.assert_not_called()
|
||||||
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, [], instance)
|
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, instance)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -442,9 +442,7 @@ def test_shared_dependencies_launch(controlplane_instance_group, job_template_fa
|
|||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
pu = p.project_updates.first()
|
pu = p.project_updates.first()
|
||||||
iu = ii.inventory_updates.first()
|
iu = ii.inventory_updates.first()
|
||||||
TaskManager.start_task.assert_has_calls(
|
TaskManager.start_task.assert_has_calls([mock.call(iu, controlplane_instance_group, instance), mock.call(pu, controlplane_instance_group, instance)])
|
||||||
[mock.call(iu, controlplane_instance_group, [j1, j2], instance), mock.call(pu, controlplane_instance_group, [j1, j2], instance)]
|
|
||||||
)
|
|
||||||
pu.status = "successful"
|
pu.status = "successful"
|
||||||
pu.finished = pu.created + timedelta(seconds=1)
|
pu.finished = pu.created + timedelta(seconds=1)
|
||||||
pu.save()
|
pu.save()
|
||||||
@@ -453,9 +451,7 @@ def test_shared_dependencies_launch(controlplane_instance_group, job_template_fa
|
|||||||
iu.save()
|
iu.save()
|
||||||
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_has_calls(
|
TaskManager.start_task.assert_has_calls([mock.call(j1, controlplane_instance_group, instance), mock.call(j2, controlplane_instance_group, instance)])
|
||||||
[mock.call(j1, controlplane_instance_group, [], instance), mock.call(j2, controlplane_instance_group, [], instance)]
|
|
||||||
)
|
|
||||||
pu = [x for x in p.project_updates.all()]
|
pu = [x for x in p.project_updates.all()]
|
||||||
iu = [x for x in ii.inventory_updates.all()]
|
iu = [x for x in ii.inventory_updates.all()]
|
||||||
assert len(pu) == 1
|
assert len(pu) == 1
|
||||||
@@ -479,7 +475,7 @@ def test_job_not_blocking_project_update(controlplane_instance_group, job_templa
|
|||||||
project_update.status = "pending"
|
project_update.status = "pending"
|
||||||
project_update.save()
|
project_update.save()
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(project_update, controlplane_instance_group, [], instance)
|
TaskManager.start_task.assert_called_once_with(project_update, controlplane_instance_group, instance)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -503,7 +499,7 @@ def test_job_not_blocking_inventory_update(controlplane_instance_group, job_temp
|
|||||||
|
|
||||||
DependencyManager().schedule()
|
DependencyManager().schedule()
|
||||||
TaskManager().schedule()
|
TaskManager().schedule()
|
||||||
TaskManager.start_task.assert_called_once_with(inventory_update, controlplane_instance_group, [], instance)
|
TaskManager.start_task.assert_called_once_with(inventory_update, controlplane_instance_group, instance)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import tempfile
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from awx.main.tasks.jobs import RunJob
|
from awx.main.tasks.jobs import RunJob
|
||||||
from awx.main.tasks.system import execution_node_health_check, _cleanup_images_and_files
|
from awx.main.tasks.system import execution_node_health_check, _cleanup_images_and_files, handle_work_error
|
||||||
from awx.main.models import Instance, Job
|
from awx.main.models import Instance, Job, InventoryUpdate, ProjectUpdate
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -74,3 +74,17 @@ def test_does_not_run_reaped_job(mocker, mock_me):
|
|||||||
job.refresh_from_db()
|
job.refresh_from_db()
|
||||||
assert job.status == 'failed'
|
assert job.status == 'failed'
|
||||||
mock_run.assert_not_called()
|
mock_run.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_handle_work_error_nested(project, inventory_source):
|
||||||
|
pu = ProjectUpdate.objects.create(status='failed', project=project, celery_task_id='1234')
|
||||||
|
iu = InventoryUpdate.objects.create(status='pending', inventory_source=inventory_source, source='scm')
|
||||||
|
job = Job.objects.create(status='pending')
|
||||||
|
iu.dependent_jobs.add(pu)
|
||||||
|
job.dependent_jobs.add(pu, iu)
|
||||||
|
handle_work_error({'type': 'project_update', 'id': pu.id})
|
||||||
|
iu.refresh_from_db()
|
||||||
|
job.refresh_from_db()
|
||||||
|
assert iu.job_explanation == f'Previous Task Failed: {{"job_type": "project_update", "job_name": "", "job_id": "{pu.id}"}}'
|
||||||
|
assert job.job_explanation == f'Previous Task Failed: {{"job_type": "inventory_update", "job_name": "", "job_id": "{iu.id}"}}'
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ ColorHandler = logging.StreamHandler
|
|||||||
if settings.COLOR_LOGS is True:
|
if settings.COLOR_LOGS is True:
|
||||||
try:
|
try:
|
||||||
from logutils.colorize import ColorizingStreamHandler
|
from logutils.colorize import ColorizingStreamHandler
|
||||||
|
import colorama
|
||||||
|
|
||||||
|
colorama.deinit()
|
||||||
|
colorama.init(wrap=False, convert=False, strip=False)
|
||||||
|
|
||||||
class ColorHandler(ColorizingStreamHandler):
|
class ColorHandler(ColorizingStreamHandler):
|
||||||
def colorize(self, line, record):
|
def colorize(self, line, record):
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ TEMPLATES = [
|
|||||||
],
|
],
|
||||||
'builtins': ['awx.main.templatetags.swagger'],
|
'builtins': ['awx.main.templatetags.swagger'],
|
||||||
},
|
},
|
||||||
'DIRS': [os.path.join(BASE_DIR, 'templates'), os.path.join(BASE_DIR, 'ui', 'build'), os.path.join(BASE_DIR, 'ui', 'public')],
|
'DIRS': [os.path.join(BASE_DIR, 'templates'), '/var/lib/awx/public/static/controller/', os.path.join(BASE_DIR, 'ui', 'build'), os.path.join(BASE_DIR, 'ui', 'public')],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -418,6 +418,9 @@ AUTH_BASIC_ENABLED = True
|
|||||||
# when trying to access a UI page that requries authentication.
|
# when trying to access a UI page that requries authentication.
|
||||||
LOGIN_REDIRECT_OVERRIDE = ''
|
LOGIN_REDIRECT_OVERRIDE = ''
|
||||||
|
|
||||||
|
# Note: This setting may be overridden by database settings.
|
||||||
|
ALLOW_METRICS_FOR_ANONYMOUS_USERS = False
|
||||||
|
|
||||||
DEVSERVER_DEFAULT_ADDR = '0.0.0.0'
|
DEVSERVER_DEFAULT_ADDR = '0.0.0.0'
|
||||||
DEVSERVER_DEFAULT_PORT = '8013'
|
DEVSERVER_DEFAULT_PORT = '8013'
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ if 'sqlite3' not in DATABASES['default']['ENGINE']: # noqa
|
|||||||
# this needs to stay at the bottom of this file
|
# this needs to stay at the bottom of this file
|
||||||
try:
|
try:
|
||||||
if os.getenv('AWX_KUBE_DEVEL', False):
|
if os.getenv('AWX_KUBE_DEVEL', False):
|
||||||
include(optional('minikube.py'), scope=locals())
|
include(optional('development_kube.py'), scope=locals())
|
||||||
else:
|
else:
|
||||||
include(optional('local_*.py'), scope=locals())
|
include(optional('local_*.py'), scope=locals())
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
BROADCAST_WEBSOCKET_SECRET = '🤖starscream🤖'
|
BROADCAST_WEBSOCKET_SECRET = '🤖starscream🤖'
|
||||||
BROADCAST_WEBSOCKET_PORT = 8013
|
BROADCAST_WEBSOCKET_PORT = 8052
|
||||||
BROADCAST_WEBSOCKET_VERIFY_CERT = False
|
BROADCAST_WEBSOCKET_VERIFY_CERT = False
|
||||||
BROADCAST_WEBSOCKET_PROTOCOL = 'http'
|
BROADCAST_WEBSOCKET_PROTOCOL = 'http'
|
||||||
@@ -452,7 +452,10 @@ def on_populate_user(sender, **kwargs):
|
|||||||
remove = bool(team_opts.get('remove', True))
|
remove = bool(team_opts.get('remove', True))
|
||||||
state = _update_m2m_from_groups(ldap_user, users_opts, remove)
|
state = _update_m2m_from_groups(ldap_user, users_opts, remove)
|
||||||
if state is not None:
|
if state is not None:
|
||||||
desired_team_states[team_name] = {'member_role': state}
|
organization = team_opts['organization']
|
||||||
|
if organization not in desired_team_states:
|
||||||
|
desired_team_states[organization] = {}
|
||||||
|
desired_team_states[organization][team_name] = {'member_role': state}
|
||||||
|
|
||||||
# Check if user.profile is available, otherwise force user.save()
|
# Check if user.profile is available, otherwise force user.save()
|
||||||
try:
|
try:
|
||||||
@@ -473,16 +476,28 @@ def on_populate_user(sender, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def reconcile_users_org_team_mappings(user, desired_org_states, desired_team_states, source):
|
def reconcile_users_org_team_mappings(user, desired_org_states, desired_team_states, source):
|
||||||
|
#
|
||||||
|
# Arguments:
|
||||||
|
# user - a user object
|
||||||
|
# desired_org_states: { '<org_name>': { '<role>': <boolean> or None } }
|
||||||
|
# desired_team_states: { '<org_name>': { '<team name>': { '<role>': <boolean> or None } } }
|
||||||
|
# source - a text label indicating the "authentication adapter" for debug messages
|
||||||
|
#
|
||||||
|
# This function will load the users existing roles and then based on the deisred states modify the users roles
|
||||||
|
# True indicates the user needs to be a member of the role
|
||||||
|
# False indicates the user should not be a member of the role
|
||||||
|
# None means this function should not change the users membership of a role
|
||||||
|
#
|
||||||
from awx.main.models import Organization, Team
|
from awx.main.models import Organization, Team
|
||||||
|
|
||||||
content_types = []
|
content_types = []
|
||||||
reconcile_items = []
|
reconcile_items = []
|
||||||
if desired_org_states:
|
if desired_org_states:
|
||||||
content_types.append(ContentType.objects.get_for_model(Organization))
|
content_types.append(ContentType.objects.get_for_model(Organization))
|
||||||
reconcile_items.append(('organization', desired_org_states, Organization))
|
reconcile_items.append(('organization', desired_org_states))
|
||||||
if desired_team_states:
|
if desired_team_states:
|
||||||
content_types.append(ContentType.objects.get_for_model(Team))
|
content_types.append(ContentType.objects.get_for_model(Team))
|
||||||
reconcile_items.append(('team', desired_team_states, Team))
|
reconcile_items.append(('team', desired_team_states))
|
||||||
|
|
||||||
if not content_types:
|
if not content_types:
|
||||||
# If both desired states were empty we can simply return because there is nothing to reconcile
|
# If both desired states were empty we can simply return because there is nothing to reconcile
|
||||||
@@ -491,24 +506,39 @@ def reconcile_users_org_team_mappings(user, desired_org_states, desired_team_sta
|
|||||||
# users_roles is a flat set of IDs
|
# users_roles is a flat set of IDs
|
||||||
users_roles = set(user.roles.filter(content_type__in=content_types).values_list('pk', flat=True))
|
users_roles = set(user.roles.filter(content_type__in=content_types).values_list('pk', flat=True))
|
||||||
|
|
||||||
for object_type, desired_states, model in reconcile_items:
|
for object_type, desired_states in reconcile_items:
|
||||||
# Get all of the roles in the desired states for efficient DB extraction
|
|
||||||
roles = []
|
roles = []
|
||||||
for sub_dict in desired_states.values():
|
|
||||||
for role_name in sub_dict:
|
|
||||||
if sub_dict[role_name] is None:
|
|
||||||
continue
|
|
||||||
if role_name not in roles:
|
|
||||||
roles.append(role_name)
|
|
||||||
|
|
||||||
# Get a set of named tuples for the org/team name plus all of the roles we got above
|
# Get a set of named tuples for the org/team name plus all of the roles we got above
|
||||||
model_roles = model.objects.filter(name__in=desired_states.keys()).values_list('name', *roles, named=True)
|
if object_type == 'organization':
|
||||||
|
for sub_dict in desired_states.values():
|
||||||
|
for role_name in sub_dict:
|
||||||
|
if sub_dict[role_name] is None:
|
||||||
|
continue
|
||||||
|
if role_name not in roles:
|
||||||
|
roles.append(role_name)
|
||||||
|
model_roles = Organization.objects.filter(name__in=desired_states.keys()).values_list('name', *roles, named=True)
|
||||||
|
else:
|
||||||
|
team_names = []
|
||||||
|
for teams_dict in desired_states.values():
|
||||||
|
team_names.extend(teams_dict.keys())
|
||||||
|
for sub_dict in teams_dict.values():
|
||||||
|
for role_name in sub_dict:
|
||||||
|
if sub_dict[role_name] is None:
|
||||||
|
continue
|
||||||
|
if role_name not in roles:
|
||||||
|
roles.append(role_name)
|
||||||
|
model_roles = Team.objects.filter(name__in=team_names).values_list('name', 'organization__name', *roles, named=True)
|
||||||
|
|
||||||
for row in model_roles:
|
for row in model_roles:
|
||||||
for role_name in roles:
|
for role_name in roles:
|
||||||
desired_state = desired_states.get(row.name, {})
|
if object_type == 'organization':
|
||||||
if desired_state[role_name] is None:
|
desired_state = desired_states.get(row.name, {})
|
||||||
|
else:
|
||||||
|
desired_state = desired_states.get(row.organization__name, {}).get(row.name, {})
|
||||||
|
|
||||||
|
if desired_state.get(role_name, None) is None:
|
||||||
# The mapping was not defined for this [org/team]/role so we can just pass
|
# The mapping was not defined for this [org/team]/role so we can just pass
|
||||||
pass
|
continue
|
||||||
|
|
||||||
# If somehow the auth adapter knows about an items role but that role is not defined in the DB we are going to print a pretty error
|
# If somehow the auth adapter knows about an items role but that role is not defined in the DB we are going to print a pretty error
|
||||||
# This is your classic safety net that we should never hit; but here you are reading this comment... good luck and Godspeed.
|
# This is your classic safety net that we should never hit; but here you are reading this comment... good luck and Godspeed.
|
||||||
|
|||||||
79
awx/ui/package-lock.json
generated
79
awx/ui/package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lingui/react": "3.14.0",
|
"@lingui/react": "3.14.0",
|
||||||
"@patternfly/patternfly": "4.217.1",
|
"@patternfly/patternfly": "4.217.1",
|
||||||
"@patternfly/react-core": "^4.250.1",
|
"@patternfly/react-core": "^4.264.0",
|
||||||
"@patternfly/react-icons": "4.92.10",
|
"@patternfly/react-icons": "4.92.10",
|
||||||
"@patternfly/react-table": "4.108.0",
|
"@patternfly/react-table": "4.108.0",
|
||||||
"ace-builds": "^1.10.1",
|
"ace-builds": "^1.10.1",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"has-ansi": "5.0.1",
|
"has-ansi": "5.0.1",
|
||||||
"html-entities": "2.3.2",
|
"html-entities": "2.3.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"luxon": "^3.0.3",
|
"luxon": "^3.1.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-ace": "^10.1.0",
|
"react-ace": "^10.1.0",
|
||||||
@@ -3752,13 +3752,13 @@
|
|||||||
"integrity": "sha512-uN7JgfQsyR16YHkuGRCTIcBcnyKIqKjGkB2SGk9x1XXH3yYGenL83kpAavX9Xtozqp17KppOlybJuzcKvZMrgw=="
|
"integrity": "sha512-uN7JgfQsyR16YHkuGRCTIcBcnyKIqKjGkB2SGk9x1XXH3yYGenL83kpAavX9Xtozqp17KppOlybJuzcKvZMrgw=="
|
||||||
},
|
},
|
||||||
"node_modules/@patternfly/react-core": {
|
"node_modules/@patternfly/react-core": {
|
||||||
"version": "4.250.1",
|
"version": "4.264.0",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.250.1.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.264.0.tgz",
|
||||||
"integrity": "sha512-vAOZPQdZzYXl/vkHnHMIt1eC3nrPDdsuuErPatkNPwmSvilXuXmWP5wxoJ36FbSNRRURkprFwx52zMmWS3iHJA==",
|
"integrity": "sha512-tK0BMWxw8nhukev40HZ6q6d02pDnjX7oyA91vHa18aakJUKBWMaerqpG4NZVMoh0tPKX3aLNj+zyCwDALFAZZw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@patternfly/react-icons": "^4.92.6",
|
"@patternfly/react-icons": "^4.93.0",
|
||||||
"@patternfly/react-styles": "^4.91.6",
|
"@patternfly/react-styles": "^4.92.0",
|
||||||
"@patternfly/react-tokens": "^4.93.6",
|
"@patternfly/react-tokens": "^4.94.0",
|
||||||
"focus-trap": "6.9.2",
|
"focus-trap": "6.9.2",
|
||||||
"react-dropzone": "9.0.0",
|
"react-dropzone": "9.0.0",
|
||||||
"tippy.js": "5.1.2",
|
"tippy.js": "5.1.2",
|
||||||
@@ -3769,6 +3769,15 @@
|
|||||||
"react-dom": "^16.8 || ^17 || ^18"
|
"react-dom": "^16.8 || ^17 || ^18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@patternfly/react-core/node_modules/@patternfly/react-icons": {
|
||||||
|
"version": "4.93.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.93.0.tgz",
|
||||||
|
"integrity": "sha512-OH0vORVioL+HLWMEog8/3u8jsiMCeJ0pFpvRKRhy5Uk4CdAe40k1SOBvXJP6opr+O8TLbz0q3bm8Jsh/bPaCuQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8 || ^17 || ^18",
|
||||||
|
"react-dom": "^16.8 || ^17 || ^18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@patternfly/react-core/node_modules/tslib": {
|
"node_modules/@patternfly/react-core/node_modules/tslib": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||||
@@ -3784,9 +3793,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@patternfly/react-styles": {
|
"node_modules/@patternfly/react-styles": {
|
||||||
"version": "4.91.10",
|
"version": "4.92.0",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.91.10.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.92.0.tgz",
|
||||||
"integrity": "sha512-fAG4Vjp63ohiR92F4e/Gkw5q1DSSckHKqdnEF75KUpSSBORzYP0EKMpupSd6ItpQFJw3iWs3MJi3/KIAAfU1Jw=="
|
"integrity": "sha512-B/f6iyu8UEN1+wRxdC4sLIhvJeyL8SqInDXZmwOIqK8uPJ8Lze7qrbVhkkVzbMF37/oDPVa6dZH8qZFq062LEA=="
|
||||||
},
|
},
|
||||||
"node_modules/@patternfly/react-table": {
|
"node_modules/@patternfly/react-table": {
|
||||||
"version": "4.108.0",
|
"version": "4.108.0",
|
||||||
@@ -3811,9 +3820,9 @@
|
|||||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@patternfly/react-tokens": {
|
"node_modules/@patternfly/react-tokens": {
|
||||||
"version": "4.93.10",
|
"version": "4.94.0",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.93.10.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.94.0.tgz",
|
||||||
"integrity": "sha512-F+j1irDc9M6zvY6qNtDryhbpnHz3R8ymHRdGelNHQzPTIK88YSWEnT1c9iUI+uM/iuZol7sJmO5STtg2aPIDRQ=="
|
"integrity": "sha512-fYXxUJZnzpn89K2zzHF0cSncZZVGKrohdb5f5T1wzxwU2NZPVGpvr88xhm+V2Y/fSrrTPwXcP3IIdtNOOtJdZw=="
|
||||||
},
|
},
|
||||||
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
|
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
@@ -15468,9 +15477,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/luxon": {
|
"node_modules/luxon": {
|
||||||
"version": "3.0.3",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.1.tgz",
|
||||||
"integrity": "sha512-+EfHWnF+UT7GgTnq5zXg3ldnTKL2zdv7QJgsU5bjjpbH17E3qi/puMhQyJVYuCq+FRkogvB5WB6iVvUr+E4a7w==",
|
"integrity": "sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -25094,19 +25103,25 @@
|
|||||||
"integrity": "sha512-uN7JgfQsyR16YHkuGRCTIcBcnyKIqKjGkB2SGk9x1XXH3yYGenL83kpAavX9Xtozqp17KppOlybJuzcKvZMrgw=="
|
"integrity": "sha512-uN7JgfQsyR16YHkuGRCTIcBcnyKIqKjGkB2SGk9x1XXH3yYGenL83kpAavX9Xtozqp17KppOlybJuzcKvZMrgw=="
|
||||||
},
|
},
|
||||||
"@patternfly/react-core": {
|
"@patternfly/react-core": {
|
||||||
"version": "4.250.1",
|
"version": "4.264.0",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.250.1.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.264.0.tgz",
|
||||||
"integrity": "sha512-vAOZPQdZzYXl/vkHnHMIt1eC3nrPDdsuuErPatkNPwmSvilXuXmWP5wxoJ36FbSNRRURkprFwx52zMmWS3iHJA==",
|
"integrity": "sha512-tK0BMWxw8nhukev40HZ6q6d02pDnjX7oyA91vHa18aakJUKBWMaerqpG4NZVMoh0tPKX3aLNj+zyCwDALFAZZw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@patternfly/react-icons": "^4.92.6",
|
"@patternfly/react-icons": "^4.93.0",
|
||||||
"@patternfly/react-styles": "^4.91.6",
|
"@patternfly/react-styles": "^4.92.0",
|
||||||
"@patternfly/react-tokens": "^4.93.6",
|
"@patternfly/react-tokens": "^4.94.0",
|
||||||
"focus-trap": "6.9.2",
|
"focus-trap": "6.9.2",
|
||||||
"react-dropzone": "9.0.0",
|
"react-dropzone": "9.0.0",
|
||||||
"tippy.js": "5.1.2",
|
"tippy.js": "5.1.2",
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@patternfly/react-icons": {
|
||||||
|
"version": "4.93.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.93.0.tgz",
|
||||||
|
"integrity": "sha512-OH0vORVioL+HLWMEog8/3u8jsiMCeJ0pFpvRKRhy5Uk4CdAe40k1SOBvXJP6opr+O8TLbz0q3bm8Jsh/bPaCuQ==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||||
@@ -25121,9 +25136,9 @@
|
|||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@patternfly/react-styles": {
|
"@patternfly/react-styles": {
|
||||||
"version": "4.91.10",
|
"version": "4.92.0",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.91.10.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.92.0.tgz",
|
||||||
"integrity": "sha512-fAG4Vjp63ohiR92F4e/Gkw5q1DSSckHKqdnEF75KUpSSBORzYP0EKMpupSd6ItpQFJw3iWs3MJi3/KIAAfU1Jw=="
|
"integrity": "sha512-B/f6iyu8UEN1+wRxdC4sLIhvJeyL8SqInDXZmwOIqK8uPJ8Lze7qrbVhkkVzbMF37/oDPVa6dZH8qZFq062LEA=="
|
||||||
},
|
},
|
||||||
"@patternfly/react-table": {
|
"@patternfly/react-table": {
|
||||||
"version": "4.108.0",
|
"version": "4.108.0",
|
||||||
@@ -25146,9 +25161,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@patternfly/react-tokens": {
|
"@patternfly/react-tokens": {
|
||||||
"version": "4.93.10",
|
"version": "4.94.0",
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.93.10.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.94.0.tgz",
|
||||||
"integrity": "sha512-F+j1irDc9M6zvY6qNtDryhbpnHz3R8ymHRdGelNHQzPTIK88YSWEnT1c9iUI+uM/iuZol7sJmO5STtg2aPIDRQ=="
|
"integrity": "sha512-fYXxUJZnzpn89K2zzHF0cSncZZVGKrohdb5f5T1wzxwU2NZPVGpvr88xhm+V2Y/fSrrTPwXcP3IIdtNOOtJdZw=="
|
||||||
},
|
},
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": {
|
"@pmmmwh/react-refresh-webpack-plugin": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
@@ -34210,9 +34225,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"luxon": {
|
"luxon": {
|
||||||
"version": "3.0.3",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.1.tgz",
|
||||||
"integrity": "sha512-+EfHWnF+UT7GgTnq5zXg3ldnTKL2zdv7QJgsU5bjjpbH17E3qi/puMhQyJVYuCq+FRkogvB5WB6iVvUr+E4a7w=="
|
"integrity": "sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw=="
|
||||||
},
|
},
|
||||||
"lz-string": {
|
"lz-string": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lingui/react": "3.14.0",
|
"@lingui/react": "3.14.0",
|
||||||
"@patternfly/patternfly": "4.217.1",
|
"@patternfly/patternfly": "4.217.1",
|
||||||
"@patternfly/react-core": "^4.250.1",
|
"@patternfly/react-core": "^4.264.0",
|
||||||
"@patternfly/react-icons": "4.92.10",
|
"@patternfly/react-icons": "4.92.10",
|
||||||
"@patternfly/react-table": "4.108.0",
|
"@patternfly/react-table": "4.108.0",
|
||||||
"ace-builds": "^1.10.1",
|
"ace-builds": "^1.10.1",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"has-ansi": "5.0.1",
|
"has-ansi": "5.0.1",
|
||||||
"html-entities": "2.3.2",
|
"html-entities": "2.3.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"luxon": "^3.0.3",
|
"luxon": "^3.1.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-ace": "^10.1.0",
|
"react-ace": "^10.1.0",
|
||||||
|
|||||||
@@ -153,6 +153,10 @@ function CredentialsStep({
|
|||||||
}))}
|
}))}
|
||||||
value={selectedType && selectedType.id}
|
value={selectedType && selectedType.id}
|
||||||
onChange={(e, id) => {
|
onChange={(e, id) => {
|
||||||
|
// Reset query params when the category of credentials is changed
|
||||||
|
history.replace({
|
||||||
|
search: '',
|
||||||
|
});
|
||||||
setSelectedType(types.find((o) => o.id === parseInt(id, 10)));
|
setSelectedType(types.find((o) => o.id === parseInt(id, 10)));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { act } from 'react-dom/test-utils';
|
|||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import { CredentialsAPI, CredentialTypesAPI } from 'api';
|
import { CredentialsAPI, CredentialTypesAPI } from 'api';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import CredentialsStep from './CredentialsStep';
|
import CredentialsStep from './CredentialsStep';
|
||||||
|
|
||||||
jest.mock('../../../api/models/CredentialTypes');
|
jest.mock('../../../api/models/CredentialTypes');
|
||||||
@@ -164,6 +165,41 @@ describe('CredentialsStep', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should reset query params (credential.page) when selected credential type is changed', async () => {
|
||||||
|
let wrapper;
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['?credential.page=2'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik>
|
||||||
|
<CredentialsStep allowCredentialsWithPasswords />
|
||||||
|
</Formik>,
|
||||||
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(CredentialsAPI.read).toHaveBeenCalledWith({
|
||||||
|
credential_type: 1,
|
||||||
|
order_by: 'name',
|
||||||
|
page: 2,
|
||||||
|
page_size: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('AnsibleSelect').invoke('onChange')({}, 3);
|
||||||
|
});
|
||||||
|
expect(CredentialsAPI.read).toHaveBeenCalledWith({
|
||||||
|
credential_type: 3,
|
||||||
|
order_by: 'name',
|
||||||
|
page: 1,
|
||||||
|
page_size: 5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("error should be shown when a credential that prompts for passwords is selected on a step that doesn't allow it", async () => {
|
test("error should be shown when a credential that prompts for passwords is selected on a step that doesn't allow it", async () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
@@ -173,6 +173,10 @@ function MultiCredentialsLookup({
|
|||||||
}))}
|
}))}
|
||||||
value={selectedType && selectedType.id}
|
value={selectedType && selectedType.id}
|
||||||
onChange={(e, id) => {
|
onChange={(e, id) => {
|
||||||
|
// Reset query params when the category of credentials is changed
|
||||||
|
history.replace({
|
||||||
|
search: '',
|
||||||
|
});
|
||||||
setSelectedType(
|
setSelectedType(
|
||||||
credentialTypes.find((o) => o.id === parseInt(id, 10))
|
credentialTypes.find((o) => o.id === parseInt(id, 10))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../testUtils/enzymeHelpers';
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import MultiCredentialsLookup from './MultiCredentialsLookup';
|
import MultiCredentialsLookup from './MultiCredentialsLookup';
|
||||||
|
|
||||||
jest.mock('../../api');
|
jest.mock('../../api');
|
||||||
@@ -228,6 +229,53 @@ describe('<Formik><MultiCredentialsLookup /></Formik>', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should reset query params (credentials.page) when selected credential type is changed', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['?credentials.page=2'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik>
|
||||||
|
<MultiCredentialsLookup
|
||||||
|
value={credentials}
|
||||||
|
tooltip="This is credentials look up"
|
||||||
|
onChange={() => {}}
|
||||||
|
onError={() => {}}
|
||||||
|
/>
|
||||||
|
</Formik>,
|
||||||
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const searchButton = await waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'Button[aria-label="Search"]'
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
searchButton.invoke('onClick')();
|
||||||
|
});
|
||||||
|
expect(CredentialsAPI.read).toHaveBeenCalledWith({
|
||||||
|
credential_type: 400,
|
||||||
|
order_by: 'name',
|
||||||
|
page: 2,
|
||||||
|
page_size: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const select = await waitForElement(wrapper, 'AnsibleSelect');
|
||||||
|
await act(async () => {
|
||||||
|
select.invoke('onChange')({}, 500);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(CredentialsAPI.read).toHaveBeenCalledWith({
|
||||||
|
credential_type: 500,
|
||||||
|
order_by: 'name',
|
||||||
|
page: 1,
|
||||||
|
page_size: 5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('should only add 1 credential per credential type except vault(see below)', async () => {
|
test('should only add 1 credential per credential type except vault(see below)', async () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
@@ -34,8 +34,14 @@ const QS_CONFIG = getQSConfig('template', {
|
|||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
});
|
});
|
||||||
|
|
||||||
function RelatedTemplateList({ searchParams, projectName = null }) {
|
const resources = {
|
||||||
const { id: projectId } = useParams();
|
projects: 'project',
|
||||||
|
inventories: 'inventory',
|
||||||
|
credentials: 'credentials',
|
||||||
|
};
|
||||||
|
|
||||||
|
function RelatedTemplateList({ searchParams, resourceName = null }) {
|
||||||
|
const { id } = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { addToast, Toast, toastProps } = useToast();
|
const { addToast, Toast, toastProps } = useToast();
|
||||||
|
|
||||||
@@ -129,12 +135,19 @@ function RelatedTemplateList({ searchParams, projectName = null }) {
|
|||||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
|
|
||||||
let linkTo = '';
|
let linkTo = '';
|
||||||
|
if (resourceName) {
|
||||||
if (projectName) {
|
const queryString = {
|
||||||
const qs = encodeQueryString({
|
resource_id: id,
|
||||||
project_id: projectId,
|
resource_name: resourceName,
|
||||||
project_name: projectName,
|
resource_type: resources[location.pathname.split('/')[1]],
|
||||||
});
|
resource_kind: null,
|
||||||
|
};
|
||||||
|
if (Array.isArray(resourceName)) {
|
||||||
|
const [name, kind] = resourceName;
|
||||||
|
queryString.resource_name = name;
|
||||||
|
queryString.resource_kind = kind;
|
||||||
|
}
|
||||||
|
const qs = encodeQueryString(queryString);
|
||||||
linkTo = `/templates/job_template/add/?${qs}`;
|
linkTo = `/templates/job_template/add/?${qs}`;
|
||||||
} else {
|
} else {
|
||||||
linkTo = '/templates/job_template/add';
|
linkTo = '/templates/job_template/add';
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
@@ -420,7 +420,7 @@ describe('<AdvancedSearch />', () => {
|
|||||||
const selectOptions = wrapper.find(
|
const selectOptions = wrapper.find(
|
||||||
'Select[aria-label="Related search type"] SelectOption'
|
'Select[aria-label="Related search type"] SelectOption'
|
||||||
);
|
);
|
||||||
expect(selectOptions).toHaveLength(2);
|
expect(selectOptions).toHaveLength(3);
|
||||||
expect(
|
expect(
|
||||||
selectOptions.find('SelectOption[id="name-option-select"]').prop('value')
|
selectOptions.find('SelectOption[id="name-option-select"]').prop('value')
|
||||||
).toBe('name__icontains');
|
).toBe('name__icontains');
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ function RelatedLookupTypeInput({
|
|||||||
value="name__icontains"
|
value="name__icontains"
|
||||||
description={t`Fuzzy search on name field.`}
|
description={t`Fuzzy search on name field.`}
|
||||||
/>
|
/>
|
||||||
|
<SelectOption
|
||||||
|
id="name-exact-option-select"
|
||||||
|
key="name"
|
||||||
|
value="name"
|
||||||
|
description={t`Exact search on name field.`}
|
||||||
|
/>
|
||||||
<SelectOption
|
<SelectOption
|
||||||
id="id-option-select"
|
id="id-option-select"
|
||||||
key="id"
|
key="id"
|
||||||
|
|||||||
@@ -22,6 +22,16 @@ import { CredentialsAPI } from 'api';
|
|||||||
import CredentialDetail from './CredentialDetail';
|
import CredentialDetail from './CredentialDetail';
|
||||||
import CredentialEdit from './CredentialEdit';
|
import CredentialEdit from './CredentialEdit';
|
||||||
|
|
||||||
|
const jobTemplateCredentialTypes = [
|
||||||
|
'machine',
|
||||||
|
'cloud',
|
||||||
|
'net',
|
||||||
|
'ssh',
|
||||||
|
'vault',
|
||||||
|
'kubernetes',
|
||||||
|
'cryptography',
|
||||||
|
];
|
||||||
|
|
||||||
function Credential({ setBreadcrumb }) {
|
function Credential({ setBreadcrumb }) {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
@@ -75,13 +85,14 @@ function Credential({ setBreadcrumb }) {
|
|||||||
link: `/credentials/${id}/access`,
|
link: `/credentials/${id}/access`,
|
||||||
id: 1,
|
id: 1,
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
if (jobTemplateCredentialTypes.includes(credential?.kind)) {
|
||||||
|
tabsArray.push({
|
||||||
name: t`Job Templates`,
|
name: t`Job Templates`,
|
||||||
link: `/credentials/${id}/job_templates`,
|
link: `/credentials/${id}/job_templates`,
|
||||||
id: 2,
|
id: 2,
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
let showCardHeader = true;
|
let showCardHeader = true;
|
||||||
|
|
||||||
if (pathname.endsWith('edit') || pathname.endsWith('add')) {
|
if (pathname.endsWith('edit') || pathname.endsWith('add')) {
|
||||||
@@ -133,6 +144,7 @@ function Credential({ setBreadcrumb }) {
|
|||||||
<Route key="job_templates" path="/credentials/:id/job_templates">
|
<Route key="job_templates" path="/credentials/:id/job_templates">
|
||||||
<RelatedTemplateList
|
<RelatedTemplateList
|
||||||
searchParams={{ credentials__id: credential.id }}
|
searchParams={{ credentials__id: credential.id }}
|
||||||
|
resourceName={[credential.name, credential.kind]}
|
||||||
/>
|
/>
|
||||||
</Route>,
|
</Route>,
|
||||||
<Route key="not-found" path="*">
|
<Route key="not-found" path="*">
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../testUtils/enzymeHelpers';
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
import mockCredential from './shared/data.scmCredential.json';
|
import mockMachineCredential from './shared/data.machineCredential.json';
|
||||||
|
import mockSCMCredential from './shared/data.scmCredential.json';
|
||||||
import Credential from './Credential';
|
import Credential from './Credential';
|
||||||
|
|
||||||
jest.mock('../../api');
|
jest.mock('../../api');
|
||||||
@@ -21,13 +22,10 @@ jest.mock('react-router-dom', () => ({
|
|||||||
describe('<Credential />', () => {
|
describe('<Credential />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
beforeEach(() => {
|
test('initially renders user-based machine credential successfully', async () => {
|
||||||
CredentialsAPI.readDetail.mockResolvedValueOnce({
|
CredentialsAPI.readDetail.mockResolvedValueOnce({
|
||||||
data: mockCredential,
|
data: mockMachineCredential,
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
test('initially renders user-based credential successfully', async () => {
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<Credential setBreadcrumb={() => {}} />);
|
wrapper = mountWithContexts(<Credential setBreadcrumb={() => {}} />);
|
||||||
});
|
});
|
||||||
@@ -36,6 +34,18 @@ describe('<Credential />', () => {
|
|||||||
expect(wrapper.find('RoutedTabs li').length).toBe(4);
|
expect(wrapper.find('RoutedTabs li').length).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('initially renders user-based SCM credential successfully', async () => {
|
||||||
|
CredentialsAPI.readDetail.mockResolvedValueOnce({
|
||||||
|
data: mockSCMCredential,
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<Credential setBreadcrumb={() => {}} />);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('Credential').length).toBe(1);
|
||||||
|
expect(wrapper.find('RoutedTabs li').length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
test('should render expected tabs', async () => {
|
test('should render expected tabs', async () => {
|
||||||
const expectedTabs = [
|
const expectedTabs = [
|
||||||
'Back to Credentials',
|
'Back to Credentials',
|
||||||
|
|||||||
@@ -465,7 +465,7 @@
|
|||||||
},
|
},
|
||||||
"created": "2020-05-18T21:53:35.370730Z",
|
"created": "2020-05-18T21:53:35.370730Z",
|
||||||
"modified": "2020-05-18T21:54:05.436400Z",
|
"modified": "2020-05-18T21:54:05.436400Z",
|
||||||
"name": "CyberArk AIM Central Credential Provider Lookup",
|
"name": "CyberArk Central Credential Provider Lookup",
|
||||||
"description": "",
|
"description": "",
|
||||||
"kind": "external",
|
"kind": "external",
|
||||||
"namespace": "aim",
|
"namespace": "aim",
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t, Trans } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
|
Banner,
|
||||||
Card,
|
Card,
|
||||||
PageSection,
|
PageSection,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
TabTitleText,
|
TabTitleText,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
import { InfoCircleIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
import useRequest from 'hooks/useRequest';
|
import useRequest from 'hooks/useRequest';
|
||||||
import { DashboardAPI } from 'api';
|
import { DashboardAPI } from 'api';
|
||||||
@@ -69,6 +71,15 @@ function Dashboard() {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Banner variant="info">
|
||||||
|
<Trans>
|
||||||
|
<p>
|
||||||
|
<InfoCircleIcon /> A tech preview of the new Ansible Automation
|
||||||
|
Platform user interface can be found{' '}
|
||||||
|
<a href="/controller/dashboard">here</a>.
|
||||||
|
</p>
|
||||||
|
</Trans>
|
||||||
|
</Banner>
|
||||||
<ScreenHeader
|
<ScreenHeader
|
||||||
streamType="all"
|
streamType="all"
|
||||||
breadcrumbConfig={{ '/home': t`Dashboard` }}
|
breadcrumbConfig={{ '/home': t`Dashboard` }}
|
||||||
|
|||||||
@@ -81,35 +81,30 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
|||||||
const {
|
const {
|
||||||
data: { results },
|
data: { results },
|
||||||
} = await InstanceGroupsAPI.readInstances(instanceGroup.id);
|
} = await InstanceGroupsAPI.readInstances(instanceGroup.id);
|
||||||
let instanceDetails;
|
|
||||||
const isAssociated = results.some(
|
const isAssociated = results.some(
|
||||||
({ id: instId }) => instId === parseInt(instanceId, 10)
|
({ id: instId }) => instId === parseInt(instanceId, 10)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isAssociated) {
|
if (isAssociated) {
|
||||||
const [{ data: details }, { data: healthCheckData }] =
|
const { data: details } = await InstancesAPI.readDetail(instanceId);
|
||||||
await Promise.all([
|
if (details.node_type === 'execution') {
|
||||||
InstancesAPI.readDetail(instanceId),
|
const { data: healthCheckData } =
|
||||||
InstancesAPI.readHealthCheckDetail(instanceId),
|
await InstancesAPI.readHealthCheckDetail(instanceId);
|
||||||
]);
|
setHealthCheck(healthCheckData);
|
||||||
|
}
|
||||||
instanceDetails = details;
|
setBreadcrumb(instanceGroup, details);
|
||||||
setHealthCheck(healthCheckData);
|
setForks(
|
||||||
} else {
|
computeForks(
|
||||||
throw new Error(
|
details.mem_capacity,
|
||||||
`This instance is not associated with this instance group`
|
details.cpu_capacity,
|
||||||
|
details.capacity_adjustment
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
return { instance: details };
|
||||||
}
|
}
|
||||||
|
throw new Error(
|
||||||
setBreadcrumb(instanceGroup, instanceDetails);
|
`This instance is not associated with this instance group`
|
||||||
setForks(
|
|
||||||
computeForks(
|
|
||||||
instanceDetails.mem_capacity,
|
|
||||||
instanceDetails.cpu_capacity,
|
|
||||||
instanceDetails.capacity_adjustment
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
return { instance: instanceDetails };
|
|
||||||
}, [instanceId, setBreadcrumb, instanceGroup]),
|
}, [instanceId, setBreadcrumb, instanceGroup]),
|
||||||
{ instance: {}, isLoading: true }
|
{ instance: {}, isLoading: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ function Inventory({ setBreadcrumb }) {
|
|||||||
>
|
>
|
||||||
<RelatedTemplateList
|
<RelatedTemplateList
|
||||||
searchParams={{ inventory__id: inventory.id }}
|
searchParams={{ inventory__id: inventory.id }}
|
||||||
|
resourceName={inventory.name}
|
||||||
/>
|
/>
|
||||||
</Route>,
|
</Route>,
|
||||||
<Route path="*" key="not-found">
|
<Route path="*" key="not-found">
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function JobEvent({
|
|||||||
if (lineNumber < 0) {
|
if (lineNumber < 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const canToggle = index === toggleLineIndex;
|
const canToggle = index === toggleLineIndex && !event.isTracebackOnly;
|
||||||
return (
|
return (
|
||||||
<JobEventLine
|
<JobEventLine
|
||||||
onClick={isClickable ? onJobEventClick : undefined}
|
onClick={isClickable ? onJobEventClick : undefined}
|
||||||
@@ -55,7 +55,7 @@ function JobEvent({
|
|||||||
onToggle={onToggleCollapsed}
|
onToggle={onToggleCollapsed}
|
||||||
/>
|
/>
|
||||||
<JobEventLineNumber>
|
<JobEventLineNumber>
|
||||||
{lineNumber}
|
{!event.isTracebackOnly ? lineNumber : ''}
|
||||||
<JobEventEllipsis isCollapsed={isCollapsed && canToggle} />
|
<JobEventEllipsis isCollapsed={isCollapsed && canToggle} />
|
||||||
</JobEventLineNumber>
|
</JobEventLineNumber>
|
||||||
<JobEventLineText
|
<JobEventLineText
|
||||||
|
|||||||
@@ -187,7 +187,9 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pendingRequests = Object.values(eventByUuidRequests.current || {});
|
const pendingRequests = Object.values(eventByUuidRequests.current || {});
|
||||||
setHasContentLoading(true); // prevents "no content found" screen from flashing
|
setHasContentLoading(true); // prevents "no content found" screen from flashing
|
||||||
setIsFollowModeEnabled(false);
|
if (location.search) {
|
||||||
|
setIsFollowModeEnabled(false);
|
||||||
|
}
|
||||||
Promise.allSettled(pendingRequests).then(() => {
|
Promise.allSettled(pendingRequests).then(() => {
|
||||||
setRemoteRowCount(0);
|
setRemoteRowCount(0);
|
||||||
clearLoadedEvents();
|
clearLoadedEvents();
|
||||||
@@ -251,6 +253,9 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
});
|
});
|
||||||
const updated = oldWsEvents.concat(newEvents);
|
const updated = oldWsEvents.concat(newEvents);
|
||||||
jobSocketCounter.current = updated.length;
|
jobSocketCounter.current = updated.length;
|
||||||
|
if (!oldWsEvents.length && min > remoteRowCount + 1) {
|
||||||
|
loadJobEvents(min);
|
||||||
|
}
|
||||||
return updated.sort((a, b) => a.counter - b.counter);
|
return updated.sort((a, b) => a.counter - b.counter);
|
||||||
});
|
});
|
||||||
setCssMap((prevCssMap) => ({
|
setCssMap((prevCssMap) => ({
|
||||||
@@ -358,7 +363,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadJobEvents = async () => {
|
const loadJobEvents = async (firstWsCounter = null) => {
|
||||||
const [params, loadRange] = getEventRequestParams(job, 50, [1, 50]);
|
const [params, loadRange] = getEventRequestParams(job, 50, [1, 50]);
|
||||||
|
|
||||||
if (isMounted.current) {
|
if (isMounted.current) {
|
||||||
@@ -371,6 +376,9 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
if (isFlatMode) {
|
if (isFlatMode) {
|
||||||
params.not__stdout = '';
|
params.not__stdout = '';
|
||||||
}
|
}
|
||||||
|
if (firstWsCounter) {
|
||||||
|
params.counter__lt = firstWsCounter;
|
||||||
|
}
|
||||||
const qsParams = parseQueryString(QS_CONFIG, location.search);
|
const qsParams = parseQueryString(QS_CONFIG, location.search);
|
||||||
const eventPromise = getJobModel(job.type).readEvents(job.id, {
|
const eventPromise = getJobModel(job.type).readEvents(job.id, {
|
||||||
...params,
|
...params,
|
||||||
@@ -435,7 +443,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
if (getEvent(counter)) {
|
if (getEvent(counter)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (index > remoteRowCount && index < remoteRowCount + wsEvents.length) {
|
if (index >= remoteRowCount && index < remoteRowCount + wsEvents.length) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return currentlyLoading.includes(counter);
|
return currentlyLoading.includes(counter);
|
||||||
@@ -462,7 +470,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!event &&
|
!event &&
|
||||||
index > remoteRowCount &&
|
index >= remoteRowCount &&
|
||||||
index < remoteRowCount + wsEvents.length
|
index < remoteRowCount + wsEvents.length
|
||||||
) {
|
) {
|
||||||
event = wsEvents[index - remoteRowCount];
|
event = wsEvents[index - remoteRowCount];
|
||||||
@@ -629,10 +637,14 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
setIsFollowModeEnabled(false);
|
setIsFollowModeEnabled(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollToEnd = () => {
|
const scrollToEnd = useCallback(() => {
|
||||||
scrollToRow(-1);
|
scrollToRow(-1);
|
||||||
setTimeout(() => scrollToRow(-1), 100);
|
let timeout;
|
||||||
};
|
if (isFollowModeEnabled) {
|
||||||
|
setTimeout(() => scrollToRow(-1), 100);
|
||||||
|
}
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [isFollowModeEnabled]);
|
||||||
|
|
||||||
const handleScrollLast = () => {
|
const handleScrollLast = () => {
|
||||||
scrollToEnd();
|
scrollToEnd();
|
||||||
|
|||||||
@@ -29,8 +29,11 @@ export function prependTraceback(job, events) {
|
|||||||
start_line: 0,
|
start_line: 0,
|
||||||
};
|
};
|
||||||
const firstIndex = events.findIndex((jobEvent) => jobEvent.counter === 1);
|
const firstIndex = events.findIndex((jobEvent) => jobEvent.counter === 1);
|
||||||
if (firstIndex && events[firstIndex]?.stdout) {
|
if (firstIndex > -1) {
|
||||||
const stdoutLines = events[firstIndex].stdout.split('\r\n');
|
if (!events[firstIndex].stdout) {
|
||||||
|
events[firstIndex].isTracebackOnly = true;
|
||||||
|
}
|
||||||
|
const stdoutLines = events[firstIndex].stdout?.split('\r\n') || [];
|
||||||
stdoutLines[0] = tracebackEvent.stdout;
|
stdoutLines[0] = tracebackEvent.stdout;
|
||||||
events[firstIndex].stdout = stdoutLines.join('\r\n');
|
events[firstIndex].stdout = stdoutLines.join('\r\n');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ function Project({ setBreadcrumb }) {
|
|||||||
searchParams={{
|
searchParams={{
|
||||||
project__id: project.id,
|
project__id: project.id,
|
||||||
}}
|
}}
|
||||||
projectName={project.name}
|
resourceName={project.name}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
{project?.scm_type && project.scm_type !== '' && (
|
{project?.scm_type && project.scm_type !== '' && (
|
||||||
|
|||||||
@@ -141,14 +141,14 @@ function JobsEdit() {
|
|||||||
<FormColumnLayout>
|
<FormColumnLayout>
|
||||||
<InputField
|
<InputField
|
||||||
name="AWX_ISOLATION_BASE_PATH"
|
name="AWX_ISOLATION_BASE_PATH"
|
||||||
config={jobs.AWX_ISOLATION_BASE_PATH}
|
config={jobs.AWX_ISOLATION_BASE_PATH ?? null}
|
||||||
isRequired
|
isRequired={Boolean(options?.AWX_ISOLATION_BASE_PATH)}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="SCHEDULE_MAX_JOBS"
|
name="SCHEDULE_MAX_JOBS"
|
||||||
config={jobs.SCHEDULE_MAX_JOBS}
|
config={jobs.SCHEDULE_MAX_JOBS ?? null}
|
||||||
type="number"
|
type={options?.SCHEDULE_MAX_JOBS ? 'number' : undefined}
|
||||||
isRequired
|
isRequired={Boolean(options?.SCHEDULE_MAX_JOBS)}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
name="DEFAULT_JOB_TIMEOUT"
|
name="DEFAULT_JOB_TIMEOUT"
|
||||||
|
|||||||
@@ -122,4 +122,22 @@ describe('<JobsEdit />', () => {
|
|||||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||||
expect(wrapper.find('ContentError').length).toBe(1);
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Form input fields that are invisible (due to being set manually via a settings file) should not prevent submitting the form', async () => {
|
||||||
|
const mockOptions = Object.assign({}, mockAllOptions);
|
||||||
|
// If AWX_ISOLATION_BASE_PATH has been set in a settings file it will be absent in the PUT options
|
||||||
|
delete mockOptions['actions']['PUT']['AWX_ISOLATION_BASE_PATH'];
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SettingsProvider value={mockOptions.actions}>
|
||||||
|
<JobsEdit />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Form').invoke('onSubmit')();
|
||||||
|
});
|
||||||
|
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -397,7 +397,10 @@ const InputField = ({ name, config, type = 'text', isRequired = false }) => {
|
|||||||
};
|
};
|
||||||
InputField.propTypes = {
|
InputField.propTypes = {
|
||||||
name: string.isRequired,
|
name: string.isRequired,
|
||||||
config: shape({}).isRequired,
|
config: shape({}),
|
||||||
|
};
|
||||||
|
InputField.defaultProps = {
|
||||||
|
config: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const TextAreaField = ({ name, config, isRequired = false }) => {
|
const TextAreaField = ({ name, config, isRequired = false }) => {
|
||||||
|
|||||||
@@ -9,29 +9,31 @@ function JobTemplateAdd() {
|
|||||||
const [formSubmitError, setFormSubmitError] = useState(null);
|
const [formSubmitError, setFormSubmitError] = useState(null);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const projectParams = {
|
const resourceParams = {
|
||||||
project_id: null,
|
resource_id: null,
|
||||||
project_name: null,
|
resource_name: null,
|
||||||
|
resource_type: null,
|
||||||
|
resource_kind: null,
|
||||||
};
|
};
|
||||||
history.location.search
|
history.location.search
|
||||||
.replace(/^\?/, '')
|
.replace(/^\?/, '')
|
||||||
.split('&')
|
.split('&')
|
||||||
.map((s) => s.split('='))
|
.map((s) => s.split('='))
|
||||||
.forEach(([key, val]) => {
|
.forEach(([key, val]) => {
|
||||||
if (!(key in projectParams)) {
|
if (!(key in resourceParams)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
projectParams[key] = decodeURIComponent(val);
|
resourceParams[key] = decodeURIComponent(val);
|
||||||
});
|
});
|
||||||
|
|
||||||
let projectValues = null;
|
let resourceValues = null;
|
||||||
|
|
||||||
if (
|
if (history.location.search.includes('resource_id' && 'resource_name')) {
|
||||||
Object.values(projectParams).filter((item) => item !== null).length === 2
|
resourceValues = {
|
||||||
) {
|
id: resourceParams.resource_id,
|
||||||
projectValues = {
|
name: resourceParams.resource_name,
|
||||||
id: projectParams.project_id,
|
type: resourceParams.resource_type,
|
||||||
name: projectParams.project_name,
|
kind: resourceParams.resource_kind, // refers to credential kind
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +124,7 @@ function JobTemplateAdd() {
|
|||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
submitError={formSubmitError}
|
submitError={formSubmitError}
|
||||||
projectValues={projectValues}
|
resourceValues={resourceValues}
|
||||||
isOverrideDisabledLookup
|
isOverrideDisabledLookup
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
@@ -274,9 +274,14 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
test('should parse and pre-fill project field from query params', async () => {
|
test('should parse and pre-fill project field from query params', async () => {
|
||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: [
|
initialEntries: [
|
||||||
'/templates/job_template/add/add?project_id=6&project_name=Demo%20Project',
|
'/templates/job_template/add?resource_id=6&resource_name=Demo%20Project&resource_type=project',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
ProjectsAPI.read.mockResolvedValueOnce({
|
||||||
|
count: 1,
|
||||||
|
results: [{ name: 'foo', id: 1, allow_override: true, organization: 1 }],
|
||||||
|
});
|
||||||
|
ProjectsAPI.readOptions.mockResolvedValueOnce({});
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<JobTemplateAdd />, {
|
wrapper = mountWithContexts(<JobTemplateAdd />, {
|
||||||
@@ -284,8 +289,9 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'EmptyStateBody', (el) => el.length === 0);
|
await waitForElement(wrapper, 'EmptyStateBody', (el) => el.length === 0);
|
||||||
|
|
||||||
expect(wrapper.find('input#project').prop('value')).toEqual('Demo Project');
|
expect(wrapper.find('input#project').prop('value')).toEqual('Demo Project');
|
||||||
expect(ProjectsAPI.readPlaybooks).toBeCalledWith('6');
|
expect(ProjectsAPI.readPlaybooks).toBeCalledWith(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not call ProjectsAPI.readPlaybooks if there is no project', async () => {
|
test('should not call ProjectsAPI.readPlaybooks if there is no project', async () => {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ function WorkflowJobTemplateAdd() {
|
|||||||
limit,
|
limit,
|
||||||
job_tags,
|
job_tags,
|
||||||
skip_tags,
|
skip_tags,
|
||||||
|
scm_branch,
|
||||||
...templatePayload
|
...templatePayload
|
||||||
} = values;
|
} = values;
|
||||||
templatePayload.inventory = inventory?.id;
|
templatePayload.inventory = inventory?.id;
|
||||||
@@ -32,6 +33,7 @@ function WorkflowJobTemplateAdd() {
|
|||||||
templatePayload.limit = limit === '' ? null : limit;
|
templatePayload.limit = limit === '' ? null : limit;
|
||||||
templatePayload.job_tags = job_tags === '' ? null : job_tags;
|
templatePayload.job_tags = job_tags === '' ? null : job_tags;
|
||||||
templatePayload.skip_tags = skip_tags === '' ? null : skip_tags;
|
templatePayload.skip_tags = skip_tags === '' ? null : skip_tags;
|
||||||
|
templatePayload.scm_branch = scm_branch === '' ? null : scm_branch;
|
||||||
const organizationId =
|
const organizationId =
|
||||||
organization?.id || inventory?.summary_fields?.organization.id;
|
organization?.id || inventory?.summary_fields?.organization.id;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ describe('<WorkflowJobTemplateAdd/>', () => {
|
|||||||
job_tags: null,
|
job_tags: null,
|
||||||
limit: null,
|
limit: null,
|
||||||
organization: undefined,
|
organization: undefined,
|
||||||
scm_branch: '',
|
scm_branch: null,
|
||||||
skip_tags: null,
|
skip_tags: null,
|
||||||
webhook_credential: undefined,
|
webhook_credential: undefined,
|
||||||
webhook_service: '',
|
webhook_service: '',
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ function WorkflowJobTemplateEdit({ template }) {
|
|||||||
limit,
|
limit,
|
||||||
job_tags,
|
job_tags,
|
||||||
skip_tags,
|
skip_tags,
|
||||||
|
scm_branch,
|
||||||
...templatePayload
|
...templatePayload
|
||||||
} = values;
|
} = values;
|
||||||
templatePayload.inventory = inventory?.id || null;
|
templatePayload.inventory = inventory?.id || null;
|
||||||
@@ -38,6 +39,7 @@ function WorkflowJobTemplateEdit({ template }) {
|
|||||||
templatePayload.limit = limit === '' ? null : limit;
|
templatePayload.limit = limit === '' ? null : limit;
|
||||||
templatePayload.job_tags = job_tags === '' ? null : job_tags;
|
templatePayload.job_tags = job_tags === '' ? null : job_tags;
|
||||||
templatePayload.skip_tags = skip_tags === '' ? null : skip_tags;
|
templatePayload.skip_tags = skip_tags === '' ? null : skip_tags;
|
||||||
|
templatePayload.scm_branch = scm_branch === '' ? null : scm_branch;
|
||||||
|
|
||||||
const formOrgId =
|
const formOrgId =
|
||||||
organization?.id || inventory?.summary_fields?.organization.id || null;
|
organization?.id || inventory?.summary_fields?.organization.id || null;
|
||||||
|
|||||||
@@ -690,7 +690,7 @@ JobTemplateForm.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FormikApp = withFormik({
|
const FormikApp = withFormik({
|
||||||
mapPropsToValues({ projectValues = {}, template = {} }) {
|
mapPropsToValues({ resourceValues = null, template = {} }) {
|
||||||
const {
|
const {
|
||||||
summary_fields = {
|
summary_fields = {
|
||||||
labels: { results: [] },
|
labels: { results: [] },
|
||||||
@@ -698,7 +698,7 @@ const FormikApp = withFormik({
|
|||||||
},
|
},
|
||||||
} = template;
|
} = template;
|
||||||
|
|
||||||
return {
|
const initialValues = {
|
||||||
allow_callbacks: template.allow_callbacks || false,
|
allow_callbacks: template.allow_callbacks || false,
|
||||||
allow_simultaneous: template.allow_simultaneous || false,
|
allow_simultaneous: template.allow_simultaneous || false,
|
||||||
ask_credential_on_launch: template.ask_credential_on_launch || false,
|
ask_credential_on_launch: template.ask_credential_on_launch || false,
|
||||||
@@ -739,7 +739,7 @@ const FormikApp = withFormik({
|
|||||||
playbook: template.playbook || '',
|
playbook: template.playbook || '',
|
||||||
prevent_instance_group_fallback:
|
prevent_instance_group_fallback:
|
||||||
template.prevent_instance_group_fallback || false,
|
template.prevent_instance_group_fallback || false,
|
||||||
project: summary_fields?.project || projectValues || null,
|
project: summary_fields?.project || null,
|
||||||
scm_branch: template.scm_branch || '',
|
scm_branch: template.scm_branch || '',
|
||||||
skip_tags: template.skip_tags || '',
|
skip_tags: template.skip_tags || '',
|
||||||
timeout: template.timeout || 0,
|
timeout: template.timeout || 0,
|
||||||
@@ -756,6 +756,24 @@ const FormikApp = withFormik({
|
|||||||
execution_environment:
|
execution_environment:
|
||||||
template.summary_fields?.execution_environment || null,
|
template.summary_fields?.execution_environment || null,
|
||||||
};
|
};
|
||||||
|
if (resourceValues !== null) {
|
||||||
|
if (resourceValues.type === 'credentials') {
|
||||||
|
initialValues[resourceValues.type] = [
|
||||||
|
{
|
||||||
|
id: parseInt(resourceValues.id, 10),
|
||||||
|
name: resourceValues.name,
|
||||||
|
kind: resourceValues.kind,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
initialValues[resourceValues.type] = {
|
||||||
|
id: parseInt(resourceValues.id, 10),
|
||||||
|
name: resourceValues.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialValues;
|
||||||
},
|
},
|
||||||
handleSubmit: async (values, { props, setErrors }) => {
|
handleSubmit: async (values, { props, setErrors }) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ class IndexView(TemplateView):
|
|||||||
|
|
||||||
template_name = 'index.html'
|
template_name = 'index.html'
|
||||||
|
|
||||||
|
class ControllerView(TemplateView):
|
||||||
|
|
||||||
|
template_name = 'index_controller.html'
|
||||||
|
|
||||||
|
|
||||||
class MigrationsNotran(TemplateView):
|
class MigrationsNotran(TemplateView):
|
||||||
|
|
||||||
@@ -27,4 +31,4 @@ class MigrationsNotran(TemplateView):
|
|||||||
|
|
||||||
app_name = 'ui'
|
app_name = 'ui'
|
||||||
|
|
||||||
urlpatterns = [re_path(r'^$', IndexView.as_view(), name='index'), re_path(r'^migrations_notran/$', MigrationsNotran.as_view(), name='migrations_notran')]
|
urlpatterns = [re_path(r'^$', IndexView.as_view(), name='index'), re_path(r'^controller/.*$', ControllerView.as_view(), name='index'), re_path(r'^migrations_notran/$', MigrationsNotran.as_view(), name='migrations_notran')]
|
||||||
|
|||||||
@@ -46,90 +46,216 @@ action_groups:
|
|||||||
plugin_routing:
|
plugin_routing:
|
||||||
inventory:
|
inventory:
|
||||||
tower:
|
tower:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* plugins have been deprecated, use awx.awx.controller instead.
|
||||||
redirect: awx.awx.controller
|
redirect: awx.awx.controller
|
||||||
lookup:
|
lookup:
|
||||||
tower_api:
|
tower_api:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* plugins have been deprecated, use awx.awx.controller_api instead.
|
||||||
redirect: awx.awx.controller_api
|
redirect: awx.awx.controller_api
|
||||||
tower_schedule_rrule:
|
tower_schedule_rrule:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* plugins have been deprecated, use awx.awx.schedule_rrule instead.
|
||||||
redirect: awx.awx.schedule_rrule
|
redirect: awx.awx.schedule_rrule
|
||||||
modules:
|
modules:
|
||||||
tower_ad_hoc_command_cancel:
|
tower_ad_hoc_command_cancel:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.ad_hoc_command_cancel instead.
|
||||||
redirect: awx.awx.ad_hoc_command_cancel
|
redirect: awx.awx.ad_hoc_command_cancel
|
||||||
tower_ad_hoc_command_wait:
|
tower_ad_hoc_command_wait:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.ad_hoc_command_wait instead.
|
||||||
redirect: awx.awx.ad_hoc_command_wait
|
redirect: awx.awx.ad_hoc_command_wait
|
||||||
tower_ad_hoc_command:
|
tower_ad_hoc_command:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.ad_hoc_command instead.
|
||||||
redirect: awx.awx.ad_hoc_command
|
redirect: awx.awx.ad_hoc_command
|
||||||
tower_application:
|
tower_application:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.application instead.
|
||||||
redirect: awx.awx.application
|
redirect: awx.awx.application
|
||||||
tower_meta:
|
tower_meta:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.controller_meta instead.
|
||||||
redirect: awx.awx.controller_meta
|
redirect: awx.awx.controller_meta
|
||||||
tower_credential_input_source:
|
tower_credential_input_source:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.credential_input_source instead.
|
||||||
redirect: awx.awx.credential_input_source
|
redirect: awx.awx.credential_input_source
|
||||||
tower_credential_type:
|
tower_credential_type:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.credential_type instead.
|
||||||
redirect: awx.awx.credential_type
|
redirect: awx.awx.credential_type
|
||||||
tower_credential:
|
tower_credential:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.credential instead.
|
||||||
redirect: awx.awx.credential
|
redirect: awx.awx.credential
|
||||||
tower_execution_environment:
|
tower_execution_environment:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.execution_environment instead.
|
||||||
redirect: awx.awx.execution_environment
|
redirect: awx.awx.execution_environment
|
||||||
tower_export:
|
tower_export:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.export instead.
|
||||||
redirect: awx.awx.export
|
redirect: awx.awx.export
|
||||||
tower_group:
|
tower_group:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.group instead.
|
||||||
redirect: awx.awx.group
|
redirect: awx.awx.group
|
||||||
tower_host:
|
tower_host:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.host instead.
|
||||||
redirect: awx.awx.host
|
redirect: awx.awx.host
|
||||||
tower_import:
|
tower_import:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.import instead.
|
||||||
redirect: awx.awx.import
|
redirect: awx.awx.import
|
||||||
tower_instance_group:
|
tower_instance_group:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.instance_group instead.
|
||||||
redirect: awx.awx.instance_group
|
redirect: awx.awx.instance_group
|
||||||
tower_inventory_source_update:
|
tower_inventory_source_update:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.inventory_source_update instead.
|
||||||
redirect: awx.awx.inventory_source_update
|
redirect: awx.awx.inventory_source_update
|
||||||
tower_inventory_source:
|
tower_inventory_source:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.inventory_source instead.
|
||||||
redirect: awx.awx.inventory_source
|
redirect: awx.awx.inventory_source
|
||||||
tower_inventory:
|
tower_inventory:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.inventory instead.
|
||||||
redirect: awx.awx.inventory
|
redirect: awx.awx.inventory
|
||||||
tower_job_cancel:
|
tower_job_cancel:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.job_cancel instead.
|
||||||
redirect: awx.awx.job_cancel
|
redirect: awx.awx.job_cancel
|
||||||
tower_job_launch:
|
tower_job_launch:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.job_launch instead.
|
||||||
redirect: awx.awx.job_launch
|
redirect: awx.awx.job_launch
|
||||||
tower_job_list:
|
tower_job_list:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.job_list instead.
|
||||||
redirect: awx.awx.job_list
|
redirect: awx.awx.job_list
|
||||||
tower_job_template:
|
tower_job_template:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.job_template instead.
|
||||||
redirect: awx.awx.job_template
|
redirect: awx.awx.job_template
|
||||||
tower_job_wait:
|
tower_job_wait:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.job_wait instead.
|
||||||
redirect: awx.awx.job_wait
|
redirect: awx.awx.job_wait
|
||||||
tower_label:
|
tower_label:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.label instead.
|
||||||
redirect: awx.awx.label
|
redirect: awx.awx.label
|
||||||
tower_license:
|
tower_license:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.license instead.
|
||||||
redirect: awx.awx.license
|
redirect: awx.awx.license
|
||||||
tower_notification_template:
|
tower_notification_template:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.notification_template instead.
|
||||||
redirect: awx.awx.notification_template
|
redirect: awx.awx.notification_template
|
||||||
tower_notification:
|
tower_notification:
|
||||||
redirect: awx.awx.notification_template
|
redirect: awx.awx.notification_template
|
||||||
tower_organization:
|
tower_organization:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.organization instead.
|
||||||
redirect: awx.awx.organization
|
redirect: awx.awx.organization
|
||||||
tower_project_update:
|
tower_project_update:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.project_update instead.
|
||||||
redirect: awx.awx.project_update
|
redirect: awx.awx.project_update
|
||||||
tower_project:
|
tower_project:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.project instead.
|
||||||
redirect: awx.awx.project
|
redirect: awx.awx.project
|
||||||
tower_role:
|
tower_role:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.role instead.
|
||||||
redirect: awx.awx.role
|
redirect: awx.awx.role
|
||||||
tower_schedule:
|
tower_schedule:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.schedule instead.
|
||||||
redirect: awx.awx.schedule
|
redirect: awx.awx.schedule
|
||||||
tower_settings:
|
tower_settings:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.settings instead.
|
||||||
redirect: awx.awx.settings
|
redirect: awx.awx.settings
|
||||||
tower_team:
|
tower_team:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.team instead.
|
||||||
redirect: awx.awx.team
|
redirect: awx.awx.team
|
||||||
tower_token:
|
tower_token:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.token instead.
|
||||||
redirect: awx.awx.token
|
redirect: awx.awx.token
|
||||||
tower_user:
|
tower_user:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.user instead.
|
||||||
redirect: awx.awx.user
|
redirect: awx.awx.user
|
||||||
tower_workflow_approval:
|
tower_workflow_approval:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_approval instead.
|
||||||
redirect: awx.awx.workflow_approval
|
redirect: awx.awx.workflow_approval
|
||||||
tower_workflow_job_template_node:
|
tower_workflow_job_template_node:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_job_template_node instead.
|
||||||
redirect: awx.awx.workflow_job_template_node
|
redirect: awx.awx.workflow_job_template_node
|
||||||
tower_workflow_job_template:
|
tower_workflow_job_template:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_job_template instead.
|
||||||
redirect: awx.awx.workflow_job_template
|
redirect: awx.awx.workflow_job_template
|
||||||
tower_workflow_launch:
|
tower_workflow_launch:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_launch instead.
|
||||||
redirect: awx.awx.workflow_launch
|
redirect: awx.awx.workflow_launch
|
||||||
tower_workflow_node_wait:
|
tower_workflow_node_wait:
|
||||||
|
deprecation:
|
||||||
|
removal_date: '2022-01-23'
|
||||||
|
warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_node_wait instead.
|
||||||
redirect: awx.awx.workflow_node_wait
|
redirect: awx.awx.workflow_node_wait
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ __metaclass__ = type
|
|||||||
|
|
||||||
DOCUMENTATION = '''
|
DOCUMENTATION = '''
|
||||||
name: controller
|
name: controller
|
||||||
plugin_type: inventory
|
|
||||||
author:
|
author:
|
||||||
- Matthew Jones (@matburt)
|
- Matthew Jones (@matburt)
|
||||||
- Yunfan Zhang (@YunfanZhang42)
|
- Yunfan Zhang (@YunfanZhang42)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import absolute_import, division, print_function
|
|||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
DOCUMENTATION = """
|
DOCUMENTATION = """
|
||||||
lookup: controller_api
|
name: controller_api
|
||||||
author: John Westcott IV (@john-westcott-iv)
|
author: John Westcott IV (@john-westcott-iv)
|
||||||
short_description: Search the API for objects
|
short_description: Search the API for objects
|
||||||
requirements:
|
requirements:
|
||||||
@@ -74,7 +74,7 @@ EXAMPLES = """
|
|||||||
|
|
||||||
- name: Load the UI settings specifying the connection info
|
- name: Load the UI settings specifying the connection info
|
||||||
set_fact:
|
set_fact:
|
||||||
controller_settings: "{{ lookup('awx.awx.controller_api', 'settings/ui' host='controller.example.com',
|
controller_settings: "{{ lookup('awx.awx.controller_api', 'settings/ui', host='controller.example.com',
|
||||||
username='admin', password=my_pass_var, verify_ssl=False) }}"
|
username='admin', password=my_pass_var, verify_ssl=False) }}"
|
||||||
|
|
||||||
- name: Report the usernames of all users with admin privs
|
- name: Report the usernames of all users with admin privs
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import absolute_import, division, print_function
|
|||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
DOCUMENTATION = """
|
DOCUMENTATION = """
|
||||||
lookup: schedule_rrule
|
name: schedule_rrule
|
||||||
author: John Westcott IV (@john-westcott-iv)
|
author: John Westcott IV (@john-westcott-iv)
|
||||||
short_description: Generate an rrule string which can be used for Schedules
|
short_description: Generate an rrule string which can be used for Schedules
|
||||||
requirements:
|
requirements:
|
||||||
@@ -101,39 +101,39 @@ else:
|
|||||||
|
|
||||||
|
|
||||||
class LookupModule(LookupBase):
|
class LookupModule(LookupBase):
|
||||||
frequencies = {
|
|
||||||
'none': rrule.DAILY,
|
|
||||||
'minute': rrule.MINUTELY,
|
|
||||||
'hour': rrule.HOURLY,
|
|
||||||
'day': rrule.DAILY,
|
|
||||||
'week': rrule.WEEKLY,
|
|
||||||
'month': rrule.MONTHLY,
|
|
||||||
}
|
|
||||||
|
|
||||||
weekdays = {
|
|
||||||
'monday': rrule.MO,
|
|
||||||
'tuesday': rrule.TU,
|
|
||||||
'wednesday': rrule.WE,
|
|
||||||
'thursday': rrule.TH,
|
|
||||||
'friday': rrule.FR,
|
|
||||||
'saturday': rrule.SA,
|
|
||||||
'sunday': rrule.SU,
|
|
||||||
}
|
|
||||||
|
|
||||||
set_positions = {
|
|
||||||
'first': 1,
|
|
||||||
'second': 2,
|
|
||||||
'third': 3,
|
|
||||||
'fourth': 4,
|
|
||||||
'last': -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
# plugin constructor
|
# plugin constructor
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
if LIBRARY_IMPORT_ERROR:
|
if LIBRARY_IMPORT_ERROR:
|
||||||
raise_from(AnsibleError('{0}'.format(LIBRARY_IMPORT_ERROR)), LIBRARY_IMPORT_ERROR)
|
raise_from(AnsibleError('{0}'.format(LIBRARY_IMPORT_ERROR)), LIBRARY_IMPORT_ERROR)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.frequencies = {
|
||||||
|
'none': rrule.DAILY,
|
||||||
|
'minute': rrule.MINUTELY,
|
||||||
|
'hour': rrule.HOURLY,
|
||||||
|
'day': rrule.DAILY,
|
||||||
|
'week': rrule.WEEKLY,
|
||||||
|
'month': rrule.MONTHLY,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.weekdays = {
|
||||||
|
'monday': rrule.MO,
|
||||||
|
'tuesday': rrule.TU,
|
||||||
|
'wednesday': rrule.WE,
|
||||||
|
'thursday': rrule.TH,
|
||||||
|
'friday': rrule.FR,
|
||||||
|
'saturday': rrule.SA,
|
||||||
|
'sunday': rrule.SU,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_positions = {
|
||||||
|
'first': 1,
|
||||||
|
'second': 2,
|
||||||
|
'third': 3,
|
||||||
|
'fourth': 4,
|
||||||
|
'last': -1,
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_date_time(date_string):
|
def parse_date_time(date_string):
|
||||||
try:
|
try:
|
||||||
@@ -149,14 +149,13 @@ class LookupModule(LookupBase):
|
|||||||
|
|
||||||
return self.get_rrule(frequency, kwargs)
|
return self.get_rrule(frequency, kwargs)
|
||||||
|
|
||||||
@staticmethod
|
def get_rrule(self, frequency, kwargs):
|
||||||
def get_rrule(frequency, kwargs):
|
|
||||||
|
|
||||||
if frequency not in LookupModule.frequencies:
|
if frequency not in self.frequencies:
|
||||||
raise AnsibleError('Frequency of {0} is invalid'.format(frequency))
|
raise AnsibleError('Frequency of {0} is invalid'.format(frequency))
|
||||||
|
|
||||||
rrule_kwargs = {
|
rrule_kwargs = {
|
||||||
'freq': LookupModule.frequencies[frequency],
|
'freq': self.frequencies[frequency],
|
||||||
'interval': kwargs.get('every', 1),
|
'interval': kwargs.get('every', 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,9 +186,9 @@ class LookupModule(LookupBase):
|
|||||||
days = []
|
days = []
|
||||||
for day in kwargs['on_days'].split(','):
|
for day in kwargs['on_days'].split(','):
|
||||||
day = day.strip()
|
day = day.strip()
|
||||||
if day not in LookupModule.weekdays:
|
if day not in self.weekdays:
|
||||||
raise AnsibleError('Parameter on_days must only contain values {0}'.format(', '.join(LookupModule.weekdays.keys())))
|
raise AnsibleError('Parameter on_days must only contain values {0}'.format(', '.join(self.weekdays.keys())))
|
||||||
days.append(LookupModule.weekdays[day])
|
days.append(self.weekdays[day])
|
||||||
|
|
||||||
rrule_kwargs['byweekday'] = days
|
rrule_kwargs['byweekday'] = days
|
||||||
|
|
||||||
@@ -214,13 +213,13 @@ class LookupModule(LookupBase):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise_from(AnsibleError('on_the parameter must be two words separated by a space'), e)
|
raise_from(AnsibleError('on_the parameter must be two words separated by a space'), e)
|
||||||
|
|
||||||
if weekday not in LookupModule.weekdays:
|
if weekday not in self.weekdays:
|
||||||
raise AnsibleError('Weekday portion of on_the parameter is not valid')
|
raise AnsibleError('Weekday portion of on_the parameter is not valid')
|
||||||
if occurance not in LookupModule.set_positions:
|
if occurance not in self.set_positions:
|
||||||
raise AnsibleError('The first string of the on_the parameter is not valid')
|
raise AnsibleError('The first string of the on_the parameter is not valid')
|
||||||
|
|
||||||
rrule_kwargs['byweekday'] = LookupModule.weekdays[weekday]
|
rrule_kwargs['byweekday'] = self.weekdays[weekday]
|
||||||
rrule_kwargs['bysetpos'] = LookupModule.set_positions[occurance]
|
rrule_kwargs['bysetpos'] = self.set_positions[occurance]
|
||||||
|
|
||||||
my_rule = rrule.rrule(**rrule_kwargs)
|
my_rule = rrule.rrule(**rrule_kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import absolute_import, division, print_function
|
|||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
DOCUMENTATION = """
|
DOCUMENTATION = """
|
||||||
lookup: schedule_rruleset
|
name: schedule_rruleset
|
||||||
author: John Westcott IV (@john-westcott-iv)
|
author: John Westcott IV (@john-westcott-iv)
|
||||||
short_description: Generate an rruleset string
|
short_description: Generate an rruleset string
|
||||||
requirements:
|
requirements:
|
||||||
@@ -31,7 +31,8 @@ DOCUMENTATION = """
|
|||||||
rules:
|
rules:
|
||||||
description:
|
description:
|
||||||
- Array of rules in the rruleset
|
- Array of rules in the rruleset
|
||||||
type: array
|
type: list
|
||||||
|
elements: dict
|
||||||
required: True
|
required: True
|
||||||
suboptions:
|
suboptions:
|
||||||
frequency:
|
frequency:
|
||||||
@@ -136,40 +137,44 @@ try:
|
|||||||
import pytz
|
import pytz
|
||||||
from dateutil import rrule
|
from dateutil import rrule
|
||||||
except ImportError as imp_exc:
|
except ImportError as imp_exc:
|
||||||
raise_from(AnsibleError('{0}'.format(imp_exc)), imp_exc)
|
LIBRARY_IMPORT_ERROR = imp_exc
|
||||||
|
else:
|
||||||
|
LIBRARY_IMPORT_ERROR = None
|
||||||
|
|
||||||
|
|
||||||
class LookupModule(LookupBase):
|
class LookupModule(LookupBase):
|
||||||
frequencies = {
|
|
||||||
'none': rrule.DAILY,
|
|
||||||
'minute': rrule.MINUTELY,
|
|
||||||
'hour': rrule.HOURLY,
|
|
||||||
'day': rrule.DAILY,
|
|
||||||
'week': rrule.WEEKLY,
|
|
||||||
'month': rrule.MONTHLY,
|
|
||||||
}
|
|
||||||
|
|
||||||
weekdays = {
|
|
||||||
'monday': rrule.MO,
|
|
||||||
'tuesday': rrule.TU,
|
|
||||||
'wednesday': rrule.WE,
|
|
||||||
'thursday': rrule.TH,
|
|
||||||
'friday': rrule.FR,
|
|
||||||
'saturday': rrule.SA,
|
|
||||||
'sunday': rrule.SU,
|
|
||||||
}
|
|
||||||
|
|
||||||
set_positions = {
|
|
||||||
'first': 1,
|
|
||||||
'second': 2,
|
|
||||||
'third': 3,
|
|
||||||
'fourth': 4,
|
|
||||||
'last': -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
# plugin constructor
|
# plugin constructor
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
if LIBRARY_IMPORT_ERROR:
|
||||||
|
raise_from(AnsibleError('{0}'.format(LIBRARY_IMPORT_ERROR)), LIBRARY_IMPORT_ERROR)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.frequencies = {
|
||||||
|
'none': rrule.DAILY,
|
||||||
|
'minute': rrule.MINUTELY,
|
||||||
|
'hour': rrule.HOURLY,
|
||||||
|
'day': rrule.DAILY,
|
||||||
|
'week': rrule.WEEKLY,
|
||||||
|
'month': rrule.MONTHLY,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.weekdays = {
|
||||||
|
'monday': rrule.MO,
|
||||||
|
'tuesday': rrule.TU,
|
||||||
|
'wednesday': rrule.WE,
|
||||||
|
'thursday': rrule.TH,
|
||||||
|
'friday': rrule.FR,
|
||||||
|
'saturday': rrule.SA,
|
||||||
|
'sunday': rrule.SU,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_positions = {
|
||||||
|
'first': 1,
|
||||||
|
'second': 2,
|
||||||
|
'third': 3,
|
||||||
|
'fourth': 4,
|
||||||
|
'last': -1,
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_date_time(date_string):
|
def parse_date_time(date_string):
|
||||||
@@ -188,14 +193,14 @@ class LookupModule(LookupBase):
|
|||||||
# something: [1,2,3] - A list of ints
|
# something: [1,2,3] - A list of ints
|
||||||
return_values = []
|
return_values = []
|
||||||
# If they give us a single int, lets make it a list of ints
|
# If they give us a single int, lets make it a list of ints
|
||||||
if type(rule[field_name]) == int:
|
if isinstance(rule[field_name], int):
|
||||||
rule[field_name] = [rule[field_name]]
|
rule[field_name] = [rule[field_name]]
|
||||||
# If its not a list, we need to split it into a list
|
# If its not a list, we need to split it into a list
|
||||||
if type(rule[field_name]) != list:
|
if isinstance(rule[field_name], list):
|
||||||
rule[field_name] = rule[field_name].split(',')
|
rule[field_name] = rule[field_name].split(',')
|
||||||
for value in rule[field_name]:
|
for value in rule[field_name]:
|
||||||
# If they have a list of strs we want to strip the str incase its space delineated
|
# If they have a list of strs we want to strip the str incase its space delineated
|
||||||
if type(value) == str:
|
if isinstance(value, str):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
# If value happens to be an int (from a list of ints) we need to coerce it into a str for the re.match
|
# If value happens to be an int (from a list of ints) we need to coerce it into a str for the re.match
|
||||||
if not re.match(r"^\d+$", str(value)) or int(value) < min_value or int(value) > max_value:
|
if not re.match(r"^\d+$", str(value)) or int(value) < min_value or int(value) > max_value:
|
||||||
@@ -205,7 +210,7 @@ class LookupModule(LookupBase):
|
|||||||
|
|
||||||
def process_list(self, field_name, rule, valid_list, rule_number):
|
def process_list(self, field_name, rule, valid_list, rule_number):
|
||||||
return_values = []
|
return_values = []
|
||||||
if type(rule[field_name]) != list:
|
if isinstance(rule[field_name], list):
|
||||||
rule[field_name] = rule[field_name].split(',')
|
rule[field_name] = rule[field_name].split(',')
|
||||||
for value in rule[field_name]:
|
for value in rule[field_name]:
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
@@ -260,11 +265,11 @@ class LookupModule(LookupBase):
|
|||||||
frequency = rule.get('frequency', None)
|
frequency = rule.get('frequency', None)
|
||||||
if not frequency:
|
if not frequency:
|
||||||
raise AnsibleError("Rule {0} is missing a frequency".format(rule_number))
|
raise AnsibleError("Rule {0} is missing a frequency".format(rule_number))
|
||||||
if frequency not in LookupModule.frequencies:
|
if frequency not in self.frequencies:
|
||||||
raise AnsibleError('Frequency of rule {0} is invalid {1}'.format(rule_number, frequency))
|
raise AnsibleError('Frequency of rule {0} is invalid {1}'.format(rule_number, frequency))
|
||||||
|
|
||||||
rrule_kwargs = {
|
rrule_kwargs = {
|
||||||
'freq': LookupModule.frequencies[frequency],
|
'freq': self.frequencies[frequency],
|
||||||
'interval': rule.get('interval', 1),
|
'interval': rule.get('interval', 1),
|
||||||
'dtstart': start_date,
|
'dtstart': start_date,
|
||||||
}
|
}
|
||||||
@@ -287,7 +292,7 @@ class LookupModule(LookupBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if 'bysetpos' in rule:
|
if 'bysetpos' in rule:
|
||||||
rrule_kwargs['bysetpos'] = self.process_list('bysetpos', rule, LookupModule.set_positions, rule_number)
|
rrule_kwargs['bysetpos'] = self.process_list('bysetpos', rule, self.set_positions, rule_number)
|
||||||
|
|
||||||
if 'bymonth' in rule:
|
if 'bymonth' in rule:
|
||||||
rrule_kwargs['bymonth'] = self.process_integer('bymonth', rule, 1, 12, rule_number)
|
rrule_kwargs['bymonth'] = self.process_integer('bymonth', rule, 1, 12, rule_number)
|
||||||
@@ -302,7 +307,7 @@ class LookupModule(LookupBase):
|
|||||||
rrule_kwargs['byweekno'] = self.process_integer('byweekno', rule, 1, 52, rule_number)
|
rrule_kwargs['byweekno'] = self.process_integer('byweekno', rule, 1, 52, rule_number)
|
||||||
|
|
||||||
if 'byweekday' in rule:
|
if 'byweekday' in rule:
|
||||||
rrule_kwargs['byweekday'] = self.process_list('byweekday', rule, LookupModule.weekdays, rule_number)
|
rrule_kwargs['byweekday'] = self.process_list('byweekday', rule, self.weekdays, rule_number)
|
||||||
|
|
||||||
if 'byhour' in rule:
|
if 'byhour' in rule:
|
||||||
rrule_kwargs['byhour'] = self.process_integer('byhour', rule, 0, 23, rule_number)
|
rrule_kwargs['byhour'] = self.process_integer('byhour', rule, 0, 23, rule_number)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ __metaclass__ = type
|
|||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||||
from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError
|
from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError
|
||||||
|
from ansible.module_utils.parsing.convert_bool import boolean as strtobool
|
||||||
from ansible.module_utils.six import PY2
|
from ansible.module_utils.six import PY2
|
||||||
from ansible.module_utils.six import raise_from, string_types
|
from ansible.module_utils.six import raise_from, string_types
|
||||||
from ansible.module_utils.six.moves import StringIO
|
from ansible.module_utils.six.moves import StringIO
|
||||||
@@ -11,14 +12,21 @@ from ansible.module_utils.six.moves.urllib.error import HTTPError
|
|||||||
from ansible.module_utils.six.moves.http_cookiejar import CookieJar
|
from ansible.module_utils.six.moves.http_cookiejar import CookieJar
|
||||||
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode
|
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode
|
||||||
from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError
|
from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError
|
||||||
from distutils.version import LooseVersion as Version
|
|
||||||
from socket import getaddrinfo, IPPROTO_TCP
|
from socket import getaddrinfo, IPPROTO_TCP
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
from json import loads, dumps
|
from json import loads, dumps
|
||||||
from os.path import isfile, expanduser, split, join, exists, isdir
|
from os.path import isfile, expanduser, split, join, exists, isdir
|
||||||
from os import access, R_OK, getcwd
|
from os import access, R_OK, getcwd
|
||||||
from distutils.util import strtobool
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ansible.module_utils.compat.version import LooseVersion as Version
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from distutils.version import LooseVersion as Version
|
||||||
|
except ImportError:
|
||||||
|
raise AssertionError('To use this plugin or module with ansible-core 2.11, you need to use Python < 3.12 with distutils.version present')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import yaml
|
import yaml
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ options:
|
|||||||
description:
|
description:
|
||||||
- The arguments to pass to the module.
|
- The arguments to pass to the module.
|
||||||
type: str
|
type: str
|
||||||
default: ""
|
|
||||||
forks:
|
forks:
|
||||||
description:
|
description:
|
||||||
- The number of forks to use for this ad hoc execution.
|
- The number of forks to use for this ad hoc execution.
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ options:
|
|||||||
- Maximum time in seconds to wait for a job to finish.
|
- Maximum time in seconds to wait for a job to finish.
|
||||||
- Not specifying means the task will wait until the controller cancels the command.
|
- Not specifying means the task will wait until the controller cancels the command.
|
||||||
type: int
|
type: int
|
||||||
|
default: 0
|
||||||
extends_documentation_fragment: awx.awx.auth
|
extends_documentation_fragment: awx.awx.auth
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ options:
|
|||||||
- The credential type being created.
|
- The credential type being created.
|
||||||
- Can be a built-in credential type such as "Machine", or a custom credential type such as "My Credential Type"
|
- Can be a built-in credential type such as "Machine", or a custom credential type such as "My Credential Type"
|
||||||
- Choices include Amazon Web Services, Ansible Galaxy/Automation Hub API Token, Centrify Vault Credential Provider Lookup,
|
- Choices include Amazon Web Services, Ansible Galaxy/Automation Hub API Token, Centrify Vault Credential Provider Lookup,
|
||||||
Container Registry, CyberArk AIM Central Credential Provider Lookup, CyberArk Conjur Secrets Manager Lookup, Google Compute Engine,
|
Container Registry, CyberArk Central Credential Provider Lookup, CyberArk Conjur Secret Lookup, Google Compute Engine,
|
||||||
GitHub Personal Access Token, GitLab Personal Access Token, GPG Public Key, HashiCorp Vault Secret Lookup, HashiCorp Vault Signed SSH,
|
GitHub Personal Access Token, GitLab Personal Access Token, GPG Public Key, HashiCorp Vault Secret Lookup, HashiCorp Vault Signed SSH,
|
||||||
Insights, Machine, Microsoft Azure Key Vault, Microsoft Azure Resource Manager, Network, OpenShift or Kubernetes API
|
Insights, Machine, Microsoft Azure Key Vault, Microsoft Azure Resource Manager, Network, OpenShift or Kubernetes API
|
||||||
Bearer Token, OpenStack, Red Hat Ansible Automation Platform, Red Hat Satellite 6, Red Hat Virtualization, Source Control,
|
Bearer Token, OpenStack, Red Hat Ansible Automation Platform, Red Hat Satellite 6, Red Hat Virtualization, Source Control,
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ def main():
|
|||||||
name=dict(required=True),
|
name=dict(required=True),
|
||||||
new_name=dict(),
|
new_name=dict(),
|
||||||
image=dict(required=True),
|
image=dict(required=True),
|
||||||
description=dict(default=''),
|
description=dict(),
|
||||||
organization=dict(),
|
organization=dict(),
|
||||||
credential=dict(default=''),
|
credential=dict(),
|
||||||
state=dict(choices=['present', 'absent'], default='present'),
|
state=dict(choices=['present', 'absent'], default='present'),
|
||||||
pull=dict(choices=['always', 'missing', 'never'], default='missing'),
|
pull=dict(choices=['always', 'missing', 'never'], default='missing'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -86,6 +86,16 @@ options:
|
|||||||
- workflow names to export
|
- workflow names to export
|
||||||
type: list
|
type: list
|
||||||
elements: str
|
elements: str
|
||||||
|
applications:
|
||||||
|
description:
|
||||||
|
- OAuth2 application names to export
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
schedules:
|
||||||
|
description:
|
||||||
|
- schedule names to export
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
requirements:
|
requirements:
|
||||||
- "awxkit >= 9.3.0"
|
- "awxkit >= 9.3.0"
|
||||||
notes:
|
notes:
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ def main():
|
|||||||
description = module.params.get('description')
|
description = module.params.get('description')
|
||||||
state = module.params.pop('state')
|
state = module.params.pop('state')
|
||||||
preserve_existing_hosts = module.params.get('preserve_existing_hosts')
|
preserve_existing_hosts = module.params.get('preserve_existing_hosts')
|
||||||
preserve_existing_children = module.params.get('preserve_existing_groups')
|
preserve_existing_children = module.params.get('preserve_existing_children')
|
||||||
variables = module.params.get('variables')
|
variables = module.params.get('variables')
|
||||||
|
|
||||||
# Attempt to look up the related items the user specified (these will fail the module if not found)
|
# Attempt to look up the related items the user specified (these will fail the module if not found)
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ options:
|
|||||||
description:
|
description:
|
||||||
- Maximum time in seconds to wait for a job to finish (server-side).
|
- Maximum time in seconds to wait for a job to finish (server-side).
|
||||||
type: int
|
type: int
|
||||||
|
default: 0
|
||||||
job_slice_count:
|
job_slice_count:
|
||||||
description:
|
description:
|
||||||
- The number of jobs to slice into at runtime. Will cause the Job Template to launch a workflow if value is greater than 1.
|
- The number of jobs to slice into at runtime. Will cause the Job Template to launch a workflow if value is greater than 1.
|
||||||
@@ -287,7 +288,6 @@ options:
|
|||||||
description:
|
description:
|
||||||
- Branch to use in job run. Project default used if blank. Only allowed if project allow_override field is set to true.
|
- Branch to use in job run. Project default used if blank. Only allowed if project allow_override field is set to true.
|
||||||
type: str
|
type: str
|
||||||
default: ''
|
|
||||||
labels:
|
labels:
|
||||||
description:
|
description:
|
||||||
- The labels applied to this job template
|
- The labels applied to this job template
|
||||||
|
|||||||
@@ -60,12 +60,10 @@ options:
|
|||||||
description:
|
description:
|
||||||
- The branch to use for the SCM resource.
|
- The branch to use for the SCM resource.
|
||||||
type: str
|
type: str
|
||||||
default: ''
|
|
||||||
scm_refspec:
|
scm_refspec:
|
||||||
description:
|
description:
|
||||||
- The refspec to use for the SCM resource.
|
- The refspec to use for the SCM resource.
|
||||||
type: str
|
type: str
|
||||||
default: ''
|
|
||||||
credential:
|
credential:
|
||||||
description:
|
description:
|
||||||
- Name of the credential to use with this SCM resource.
|
- Name of the credential to use with this SCM resource.
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ options:
|
|||||||
- Specify C(extra_vars) for the template.
|
- Specify C(extra_vars) for the template.
|
||||||
required: False
|
required: False
|
||||||
type: dict
|
type: dict
|
||||||
default: {}
|
|
||||||
forks:
|
forks:
|
||||||
description:
|
description:
|
||||||
- Forks applied as a prompt, assuming job template prompts for forks
|
- Forks applied as a prompt, assuming job template prompts for forks
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ options:
|
|||||||
- Note This is a client side search, not an API side search
|
- Note This is a client side search, not an API side search
|
||||||
required: False
|
required: False
|
||||||
type: dict
|
type: dict
|
||||||
|
default: {}
|
||||||
extends_documentation_fragment: awx.awx.auth
|
extends_documentation_fragment: awx.awx.auth
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ options:
|
|||||||
- Optional description of this access token.
|
- Optional description of this access token.
|
||||||
required: False
|
required: False
|
||||||
type: str
|
type: str
|
||||||
default: ''
|
|
||||||
application:
|
application:
|
||||||
description:
|
description:
|
||||||
- The application tied to this token.
|
- The application tied to this token.
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
ad_hoc_command.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ad_hoc_command_cancel.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ad_hoc_command_wait.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
application.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
controller_meta.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
credential.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
credential_input_source.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
credential_type.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
execution_environment.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
group.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
host.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
instance_group.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
inventory.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
inventory_source.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
inventory_source_update.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
job_cancel.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
job_launch.py
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user