mirror of
https://github.com/ansible/awx.git
synced 2026-02-12 07:04:45 -03:30
Compare commits
297 Commits
thenets/up
...
21.13.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0c967c1b2 | ||
|
|
2ca0b7bc01 | ||
|
|
1411d11a0e | ||
|
|
2fe1ea94bd | ||
|
|
a47cfc55ab | ||
|
|
0eb9de02f3 | ||
|
|
39ee4285ce | ||
|
|
2dcda04a9e | ||
|
|
52d46c88e4 | ||
|
|
c2df22e0f0 | ||
|
|
cf21eab7f4 | ||
|
|
98b2f51c18 | ||
|
|
327352feaf | ||
|
|
ccaace8b30 | ||
|
|
2902b40084 | ||
|
|
9669b9dd2f | ||
|
|
d27aada817 | ||
|
|
2fca07ee4c | ||
|
|
335ac636b5 | ||
|
|
f4bcc03ac7 | ||
|
|
3051384f95 | ||
|
|
811ecb8673 | ||
|
|
5e28f5dca1 | ||
|
|
d088d36448 | ||
|
|
89e41597a6 | ||
|
|
283adc30a8 | ||
|
|
019e6a52fe | ||
|
|
35e5610642 | ||
|
|
3a303875bb | ||
|
|
4499a50019 | ||
|
|
3fe46e2e27 | ||
|
|
6d3f39fe92 | ||
|
|
a3233b5fdd | ||
|
|
fe3aa6ce2b | ||
|
|
77ec46f6cf | ||
|
|
b5f240ce70 | ||
|
|
fb2647ff7b | ||
|
|
35fbb94aa6 | ||
|
|
f2ab8d637c | ||
|
|
166b586591 | ||
|
|
d1c608a281 | ||
|
|
b4803ca894 | ||
|
|
ce7f597c7e | ||
|
|
23a34c5dc9 | ||
|
|
bef3da6fb2 | ||
|
|
7f50679e68 | ||
|
|
52d071f9d1 | ||
|
|
26a888547d | ||
|
|
05af2972bf | ||
|
|
60458bebfd | ||
|
|
951eee944c | ||
|
|
4630757f5f | ||
|
|
46ea031566 | ||
|
|
0d7bbb4389 | ||
|
|
1dda373aaf | ||
|
|
33c1968210 | ||
|
|
049a158638 | ||
|
|
32f7295f44 | ||
|
|
6772fb876b | ||
|
|
51112b95bc | ||
|
|
6c1d4a5cfd | ||
|
|
2e9106d8ea | ||
|
|
84822784e8 | ||
|
|
0f3adb52b1 | ||
|
|
59da9a29df | ||
|
|
a949ee048a | ||
|
|
b959bc278f | ||
|
|
052644eb9d | ||
|
|
4e18827909 | ||
|
|
59ce8c4148 | ||
|
|
3b9c04bf1e | ||
|
|
f28203913f | ||
|
|
9b2725e5fe | ||
|
|
1af955d28c | ||
|
|
0815f935ca | ||
|
|
6997876da6 | ||
|
|
93d84fe2c9 | ||
|
|
f5785976be | ||
|
|
61c7d4e4ca | ||
|
|
a2f528e6e5 | ||
|
|
058ae132cf | ||
|
|
6483575437 | ||
|
|
a15a23c1d3 | ||
|
|
ffdcb9f4dd | ||
|
|
2d9da11443 | ||
|
|
5ce6c14f74 | ||
|
|
61748c072d | ||
|
|
89dae3865d | ||
|
|
808ab9803e | ||
|
|
d64b6d4dfe | ||
|
|
c9d931ceee | ||
|
|
8fb831d3de | ||
|
|
64865af3bb | ||
|
|
9f63c99bee | ||
|
|
d7025a919c | ||
|
|
dab7d91cff | ||
|
|
61821faa00 | ||
|
|
c26d211ee0 | ||
|
|
6a79d19668 | ||
|
|
47176cb31b | ||
|
|
5163795cc0 | ||
|
|
b0a4173545 | ||
|
|
eb9431ee1f | ||
|
|
fd6605932a | ||
|
|
ea9c52aca6 | ||
|
|
a7ebce1fef | ||
|
|
5de9cf748d | ||
|
|
ebea78943d | ||
|
|
bb387f939b | ||
|
|
bda806fd03 | ||
|
|
9777ce7fb8 | ||
|
|
1e33bc4020 | ||
|
|
d8e7c59fe8 | ||
|
|
4470b80059 | ||
|
|
e9ad01e806 | ||
|
|
8a4059d266 | ||
|
|
01a7076267 | ||
|
|
32b6aec66b | ||
|
|
884ab424d5 | ||
|
|
7e55305c45 | ||
|
|
7f6f57bfee | ||
|
|
ae92f8292f | ||
|
|
51e244e183 | ||
|
|
ad4e257fdb | ||
|
|
fcf56950b3 | ||
|
|
27ea239c00 | ||
|
|
128a130b84 | ||
|
|
d75f12c001 | ||
|
|
2034eac620 | ||
|
|
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 | ||
|
|
1d87e6e04c | ||
|
|
8be739d255 | ||
|
|
ca54195099 | ||
|
|
f0fcfdde39 | ||
|
|
80b1ba4a35 | ||
|
|
51f8e362dc | ||
|
|
737d6d8c8b | ||
|
|
beaf6b6058 | ||
|
|
aad1fbcef8 | ||
|
|
0b96d617ac | ||
|
|
fe768a159b | ||
|
|
c1ebea858b | ||
|
|
da9b8135e8 | ||
|
|
76cecf3f6b | ||
|
|
7b2938f515 | ||
|
|
916b5642d2 | ||
|
|
e524d3df3e | ||
|
|
01e9a611ea | ||
|
|
5d96ee084d | ||
|
|
e2cee10767 | ||
|
|
ef29589940 | ||
|
|
cec2d2dfb9 | ||
|
|
15b7ad3570 | ||
|
|
36ff9cbc6d | ||
|
|
ed74d80ecb | ||
|
|
31c2e1a450 | ||
|
|
a0b8215c06 | ||
|
|
f88b993b18 | ||
|
|
4a7f4d0ed4 | ||
|
|
6e08c3567f | ||
|
|
adbcb5c5e4 | ||
|
|
8054c6aedc | ||
|
|
58734a33c4 | ||
|
|
2832f28014 | ||
|
|
e5057691ee | ||
|
|
a0cfd8501c | ||
|
|
99b643bd77 | ||
|
|
305b39d8e5 | ||
|
|
bb047baeba | ||
|
|
9637aad37e | ||
|
|
fbc06ec623 | ||
|
|
57430afc55 | ||
|
|
7aae7e8ed4 | ||
|
|
a67d107a58 | ||
|
|
642003e207 | ||
|
|
ec7e2284df | ||
|
|
ff7facdfa2 | ||
|
|
6df4e62132 | ||
|
|
6289bfb639 | ||
|
|
95e4b2064f | ||
|
|
48eba60be4 | ||
|
|
c7efa8b4e0 | ||
|
|
657b5cb1aa | ||
|
|
06daebbecf | ||
|
|
fb37f22bf4 | ||
|
|
71f326b705 | ||
|
|
6508ab4a33 | ||
|
|
bf871bd427 | ||
|
|
e403c603d6 | ||
|
|
4b7b3c7c7d | ||
|
|
1cdd2cad67 | ||
|
|
86856f242a | ||
|
|
65c3db8cb8 | ||
|
|
7fa9dcbc2a | ||
|
|
7cfb957de3 | ||
|
|
d0d467e863 | ||
|
|
eaccf32aa3 | ||
|
|
a8fdb22ab3 | ||
|
|
ae79f94a48 | ||
|
|
40499a4084 | ||
|
|
b36fa93005 | ||
|
|
8839b4e90b | ||
|
|
7866135d6c | ||
|
|
fe48dc412f | ||
|
|
3a25c4221f | ||
|
|
7e1be3ef94 | ||
|
|
b2f8ca09ba | ||
|
|
c7692f5c56 | ||
|
|
3b24afa7f2 | ||
|
|
2b3f3e2043 | ||
|
|
68614b83c0 | ||
|
|
a1edc75c11 | ||
|
|
4b0e7a5cde | ||
|
|
01c6ac1b14 | ||
|
|
f0481d0a60 | ||
|
|
fd2a8b8531 | ||
|
|
239959a4c9 | ||
|
|
84f2b91105 | ||
|
|
9d7b249b20 | ||
|
|
5bd15dd48d | ||
|
|
d03348c6e4 | ||
|
|
5faeff6bec | ||
|
|
b94a126c02 | ||
|
|
eedd146643 | ||
|
|
d34f6af830 | ||
|
|
163ccfd410 | ||
|
|
fa305a7bfa | ||
|
|
8b9db837ca | ||
|
|
f9bb26ad33 | ||
|
|
271613b86d | ||
|
|
ac57f5cb28 | ||
|
|
c39172f516 | ||
|
|
9b047c2af6 | ||
|
|
f0d6bc0dc8 | ||
|
|
7590301ae7 | ||
|
|
0db75fdbfd | ||
|
|
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
|
||||||
|
|
||||||
|
|||||||
82
.github/workflows/ci.yml
vendored
82
.github/workflows/ci.yml
vendored
@@ -1,7 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: CI
|
name: CI
|
||||||
env:
|
env:
|
||||||
BRANCH: ${{ github.base_ref || 'devel' }}
|
LC_ALL: "C.UTF-8" # prevent ERROR: Ansible could not initialize the preferred locale: unsupported locale setting
|
||||||
|
CI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
DEV_DOCKER_TAG_BASE: ghcr.io/${{ github.repository_owner }}
|
||||||
|
COMPOSE_TAG: ${{ github.base_ref || 'devel' }}
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
jobs:
|
jobs:
|
||||||
@@ -17,85 +20,33 @@ jobs:
|
|||||||
tests:
|
tests:
|
||||||
- name: api-test
|
- name: api-test
|
||||||
command: /start_tests.sh
|
command: /start_tests.sh
|
||||||
label: Run API Tests
|
|
||||||
- name: api-lint
|
- name: api-lint
|
||||||
command: /var/lib/awx/venv/awx/bin/tox -e linters
|
command: /var/lib/awx/venv/awx/bin/tox -e linters
|
||||||
label: Run API Linters
|
|
||||||
- name: api-swagger
|
- name: api-swagger
|
||||||
command: /start_tests.sh swagger
|
command: /start_tests.sh swagger
|
||||||
label: Generate API Reference
|
|
||||||
- name: awx-collection
|
- name: awx-collection
|
||||||
command: /start_tests.sh test_collection_all
|
command: /start_tests.sh test_collection_all
|
||||||
label: Run Collection Tests
|
|
||||||
- name: api-schema
|
- name: api-schema
|
||||||
label: Check API Schema
|
|
||||||
command: /start_tests.sh detect-schema-change SCHEMA_DIFF_BASE_BRANCH=${{ github.event.pull_request.base.ref }}
|
command: /start_tests.sh detect-schema-change SCHEMA_DIFF_BASE_BRANCH=${{ github.event.pull_request.base.ref }}
|
||||||
- name: ui-lint
|
- name: ui-lint
|
||||||
label: Run UI Linters
|
|
||||||
command: make ui-lint
|
command: make ui-lint
|
||||||
- name: ui-test-screens
|
- name: ui-test-screens
|
||||||
label: Run UI Screens Tests
|
|
||||||
command: make ui-test-screens
|
command: make ui-test-screens
|
||||||
- name: ui-test-general
|
- name: ui-test-general
|
||||||
label: Run UI General Tests
|
|
||||||
command: make ui-test-general
|
command: make ui-test-general
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Get python version from Makefile
|
- name: Run check ${{ matrix.tests.name }}
|
||||||
run: echo py_version=`make PYTHON_VERSION` >> $GITHUB_ENV
|
run: AWX_DOCKER_CMD='${{ matrix.tests.command }}' make github_ci_runner
|
||||||
|
|
||||||
- name: Install python ${{ env.py_version }}
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.py_version }}
|
|
||||||
|
|
||||||
- name: Log in to registry
|
|
||||||
run: |
|
|
||||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
|
||||||
|
|
||||||
- name: Pre-pull image to warm build cache
|
|
||||||
run: |
|
|
||||||
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} || :
|
|
||||||
|
|
||||||
- name: Build image
|
|
||||||
run: |
|
|
||||||
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ env.BRANCH }} make docker-compose-build
|
|
||||||
|
|
||||||
- name: ${{ matrix.texts.label }}
|
|
||||||
run: |
|
|
||||||
docker run -u $(id -u) --rm -v ${{ github.workspace}}:/awx_devel/:Z \
|
|
||||||
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} ${{ matrix.tests.command }}
|
|
||||||
dev-env:
|
dev-env:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Get python version from Makefile
|
|
||||||
run: echo py_version=`make PYTHON_VERSION` >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Install python ${{ env.py_version }}
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.py_version }}
|
|
||||||
|
|
||||||
- name: Log in to registry
|
|
||||||
run: |
|
|
||||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
|
||||||
|
|
||||||
- name: Pre-pull image to warm build cache
|
|
||||||
run: |
|
|
||||||
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} || :
|
|
||||||
|
|
||||||
- name: Build image
|
|
||||||
run: |
|
|
||||||
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ env.BRANCH }} make docker-compose-build
|
|
||||||
|
|
||||||
- name: Run smoke test
|
- name: Run smoke test
|
||||||
run: |
|
run: make github_ci_setup && ansible-playbook tools/docker-compose/ansible/smoke-test.yml -v
|
||||||
export DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }}
|
|
||||||
export COMPOSE_TAG=${{ env.BRANCH }}
|
|
||||||
ansible-playbook tools/docker-compose/ansible/smoke-test.yml -e repo_dir=$(pwd) -v
|
|
||||||
|
|
||||||
awx-operator:
|
awx-operator:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -144,3 +95,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
|
||||||
|
|||||||
2
.github/workflows/devel_images.yml
vendored
2
.github/workflows/devel_images.yml
vendored
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: Build/Push Development Images
|
name: Build/Push Development Images
|
||||||
|
env:
|
||||||
|
LC_ALL: "C.UTF-8" # prevent ERROR: Ansible could not initialize the preferred locale: unsupported locale setting
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
|||||||
7
.github/workflows/e2e_test.yml
vendored
7
.github/workflows/e2e_test.yml
vendored
@@ -1,9 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: E2E Tests
|
name: E2E Tests
|
||||||
|
env:
|
||||||
|
LC_ALL: "C.UTF-8" # prevent ERROR: Ansible could not initialize the preferred locale: unsupported locale setting
|
||||||
|
|
||||||
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
|
||||||
@@ -104,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: Feature branch deletion cleanup
|
name: Feature branch deletion cleanup
|
||||||
|
env:
|
||||||
|
LC_ALL: "C.UTF-8" # prevent ERROR: Ansible could not initialize the preferred locale: unsupported locale setting
|
||||||
on:
|
on:
|
||||||
delete:
|
delete:
|
||||||
branches:
|
branches:
|
||||||
|
|||||||
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
|
||||||
|
|||||||
20
.github/workflows/promote.yml
vendored
20
.github/workflows/promote.yml
vendored
@@ -1,11 +1,16 @@
|
|||||||
---
|
---
|
||||||
name: Promote Release
|
name: Promote Release
|
||||||
|
|
||||||
|
env:
|
||||||
|
LC_ALL: "C.UTF-8" # prevent ERROR: Ansible could not initialize the preferred locale: unsupported locale setting
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
promote:
|
promote:
|
||||||
|
if: endsWith(github.repository, '/awx')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout awx
|
- name: Checkout awx
|
||||||
@@ -34,9 +39,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
|
||||||
@@ -48,6 +57,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 }} \
|
||||||
@@ -70,4 +80,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 }}
|
||||||
|
|||||||
20
.github/workflows/stage.yml
vendored
20
.github/workflows/stage.yml
vendored
@@ -1,5 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: Stage Release
|
name: Stage Release
|
||||||
|
|
||||||
|
env:
|
||||||
|
LC_ALL: "C.UTF-8" # prevent ERROR: Ansible could not initialize the preferred locale: unsupported locale setting
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
@@ -17,6 +21,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stage:
|
stage:
|
||||||
|
if: endsWith(github.repository, '/awx')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
@@ -80,6 +85,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: |
|
||||||
@@ -99,6 +118,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
|
||||||
|
|||||||
4
.github/workflows/upload_schema.yml
vendored
4
.github/workflows/upload_schema.yml
vendored
@@ -1,5 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: Upload API Schema
|
name: Upload API Schema
|
||||||
|
|
||||||
|
env:
|
||||||
|
LC_ALL: "C.UTF-8" # prevent ERROR: Ansible could not initialize the preferred locale: unsupported locale setting
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ recursive-include awx/plugins *.ps1
|
|||||||
recursive-include requirements *.txt
|
recursive-include requirements *.txt
|
||||||
recursive-include requirements *.yml
|
recursive-include requirements *.yml
|
||||||
recursive-include config *
|
recursive-include config *
|
||||||
recursive-include docs/licenses *
|
recursive-include licenses *
|
||||||
recursive-exclude awx devonly.py*
|
recursive-exclude awx devonly.py*
|
||||||
recursive-exclude awx/api/tests *
|
recursive-exclude awx/api/tests *
|
||||||
recursive-exclude awx/main/tests *
|
recursive-exclude awx/main/tests *
|
||||||
|
|||||||
97
Makefile
97
Makefile
@@ -1,4 +1,5 @@
|
|||||||
PYTHON ?= python3.9
|
PYTHON ?= python3.9
|
||||||
|
DOCKER_COMPOSE ?= docker-compose
|
||||||
OFFICIAL ?= no
|
OFFICIAL ?= no
|
||||||
NODE ?= node
|
NODE ?= node
|
||||||
NPM_BIN ?= npm
|
NPM_BIN ?= npm
|
||||||
@@ -6,7 +7,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)
|
||||||
@@ -34,7 +48,7 @@ RECEPTOR_IMAGE ?= quay.io/ansible/receptor:devel
|
|||||||
SRC_ONLY_PKGS ?= cffi,pycparser,psycopg2,twilio
|
SRC_ONLY_PKGS ?= cffi,pycparser,psycopg2,twilio
|
||||||
# These should be upgraded in the AWX and Ansible venv before attempting
|
# These should be upgraded in the AWX and Ansible venv before attempting
|
||||||
# to install the actual requirements
|
# to install the actual requirements
|
||||||
VENV_BOOTSTRAP ?= pip==21.2.4 setuptools==58.2.0 setuptools_scm[toml]==6.4.2 wheel==0.36.2
|
VENV_BOOTSTRAP ?= pip==21.2.4 setuptools==65.6.3 setuptools_scm[toml]==7.0.5 wheel==0.38.4
|
||||||
|
|
||||||
NAME ?= awx
|
NAME ?= awx
|
||||||
|
|
||||||
@@ -52,7 +66,7 @@ I18N_FLAG_FILE = .i18n_built
|
|||||||
sdist \
|
sdist \
|
||||||
ui-release ui-devel \
|
ui-release ui-devel \
|
||||||
VERSION PYTHON_VERSION docker-compose-sources \
|
VERSION PYTHON_VERSION docker-compose-sources \
|
||||||
.git/hooks/pre-commit
|
.git/hooks/pre-commit github_ci_setup github_ci_runner
|
||||||
|
|
||||||
clean-tmp:
|
clean-tmp:
|
||||||
rm -rf tmp/
|
rm -rf tmp/
|
||||||
@@ -190,19 +204,7 @@ uwsgi: collectstatic
|
|||||||
@if [ "$(VENV_BASE)" ]; then \
|
@if [ "$(VENV_BASE)" ]; then \
|
||||||
. $(VENV_BASE)/awx/bin/activate; \
|
. $(VENV_BASE)/awx/bin/activate; \
|
||||||
fi; \
|
fi; \
|
||||||
uwsgi -b 32768 \
|
uwsgi /etc/tower/uwsgi.ini
|
||||||
--socket 127.0.0.1:8050 \
|
|
||||||
--module=awx.wsgi:application \
|
|
||||||
--home=/var/lib/awx/venv/awx \
|
|
||||||
--chdir=/awx_devel/ \
|
|
||||||
--vacuum \
|
|
||||||
--processes=5 \
|
|
||||||
--harakiri=120 --master \
|
|
||||||
--no-orphans \
|
|
||||||
--max-requests=1000 \
|
|
||||||
--stats /tmp/stats.socket \
|
|
||||||
--lazy-apps \
|
|
||||||
--logformat "%(addr) %(method) %(uri) - %(proto) %(status)"
|
|
||||||
|
|
||||||
awx-autoreload:
|
awx-autoreload:
|
||||||
@/awx_devel/tools/docker-compose/awx-autoreload /awx_devel/awx "$(DEV_RELOAD_COMMAND)"
|
@/awx_devel/tools/docker-compose/awx-autoreload /awx_devel/awx "$(DEV_RELOAD_COMMAND)"
|
||||||
@@ -288,19 +290,28 @@ 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
|
## Login to Github container image registry, pull image, then build image.
|
||||||
COLLECTION_TEST_TARGET ?=
|
github_ci_setup:
|
||||||
COLLECTION_PACKAGE ?= awx
|
# GITHUB_ACTOR is automatic github actions env var
|
||||||
COLLECTION_NAMESPACE ?= awx
|
# CI_GITHUB_TOKEN is defined in .github files
|
||||||
COLLECTION_INSTALL = ~/.ansible/collections/ansible_collections/$(COLLECTION_NAMESPACE)/$(COLLECTION_PACKAGE)
|
echo $(CI_GITHUB_TOKEN) | docker login ghcr.io -u $(GITHUB_ACTOR) --password-stdin
|
||||||
COLLECTION_TEMPLATE_VERSION ?= false
|
docker pull $(DEVEL_IMAGE_NAME) || : # Pre-pull image to warm build cache
|
||||||
|
make docker-compose-build
|
||||||
|
|
||||||
|
## Runs AWX_DOCKER_CMD inside a new docker container.
|
||||||
|
docker-runner:
|
||||||
|
docker run -u $(shell id -u) --rm -v $(shell pwd):/awx_devel/:Z --workdir=/awx_devel $(DEVEL_IMAGE_NAME) $(AWX_DOCKER_CMD)
|
||||||
|
|
||||||
|
## Builds image and runs AWX_DOCKER_CMD in it, mainly for .github checks.
|
||||||
|
github_ci_runner: github_ci_setup docker-runner
|
||||||
|
|
||||||
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 +341,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)
|
||||||
@@ -389,18 +405,18 @@ $(UI_BUILD_FLAG_FILE):
|
|||||||
$(PYTHON) tools/scripts/compilemessages.py
|
$(PYTHON) tools/scripts/compilemessages.py
|
||||||
$(NPM_BIN) --prefix awx/ui --loglevel warn run compile-strings
|
$(NPM_BIN) --prefix awx/ui --loglevel warn run compile-strings
|
||||||
$(NPM_BIN) --prefix awx/ui --loglevel warn run build
|
$(NPM_BIN) --prefix awx/ui --loglevel warn run build
|
||||||
mkdir -p /var/lib/awx/public/static/css
|
|
||||||
mkdir -p /var/lib/awx/public/static/js
|
|
||||||
mkdir -p /var/lib/awx/public/static/media
|
|
||||||
cp -r awx/ui/build/static/css/* /var/lib/awx/public/static/css
|
|
||||||
cp -r awx/ui/build/static/js/* /var/lib/awx/public/static/js
|
|
||||||
cp -r awx/ui/build/static/media/* /var/lib/awx/public/static/media
|
|
||||||
touch $@
|
touch $@
|
||||||
|
|
||||||
ui-release: $(UI_BUILD_FLAG_FILE)
|
ui-release: $(UI_BUILD_FLAG_FILE)
|
||||||
|
|
||||||
ui-devel: awx/ui/node_modules
|
ui-devel: awx/ui/node_modules
|
||||||
@$(MAKE) -B $(UI_BUILD_FLAG_FILE)
|
@$(MAKE) -B $(UI_BUILD_FLAG_FILE)
|
||||||
|
mkdir -p /var/lib/awx/public/static/css
|
||||||
|
mkdir -p /var/lib/awx/public/static/js
|
||||||
|
mkdir -p /var/lib/awx/public/static/media
|
||||||
|
cp -r awx/ui/build/static/css/* /var/lib/awx/public/static/css
|
||||||
|
cp -r awx/ui/build/static/js/* /var/lib/awx/public/static/js
|
||||||
|
cp -r awx/ui/build/static/media/* /var/lib/awx/public/static/media
|
||||||
|
|
||||||
ui-devel-instrumented: awx/ui/node_modules
|
ui-devel-instrumented: awx/ui/node_modules
|
||||||
$(NPM_BIN) --prefix awx/ui --loglevel warn run start-instrumented
|
$(NPM_BIN) --prefix awx/ui --loglevel warn run start-instrumented
|
||||||
@@ -482,20 +498,20 @@ docker-compose-sources: .git/hooks/pre-commit
|
|||||||
|
|
||||||
|
|
||||||
docker-compose: awx/projects docker-compose-sources
|
docker-compose: awx/projects docker-compose-sources
|
||||||
docker-compose -f tools/docker-compose/_sources/docker-compose.yml $(COMPOSE_OPTS) up $(COMPOSE_UP_OPTS) --remove-orphans
|
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml $(COMPOSE_OPTS) up $(COMPOSE_UP_OPTS) --remove-orphans
|
||||||
|
|
||||||
docker-compose-credential-plugins: awx/projects docker-compose-sources
|
docker-compose-credential-plugins: awx/projects docker-compose-sources
|
||||||
echo -e "\033[0;31mTo generate a CyberArk Conjur API key: docker exec -it tools_conjur_1 conjurctl account create quick-start\033[0m"
|
echo -e "\033[0;31mTo generate a CyberArk Conjur API key: docker exec -it tools_conjur_1 conjurctl account create quick-start\033[0m"
|
||||||
docker-compose -f tools/docker-compose/_sources/docker-compose.yml -f tools/docker-credential-plugins-override.yml up --no-recreate awx_1 --remove-orphans
|
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml -f tools/docker-credential-plugins-override.yml up --no-recreate awx_1 --remove-orphans
|
||||||
|
|
||||||
docker-compose-test: awx/projects docker-compose-sources
|
docker-compose-test: awx/projects docker-compose-sources
|
||||||
docker-compose -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports awx_1 /bin/bash
|
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports awx_1 /bin/bash
|
||||||
|
|
||||||
docker-compose-runtest: awx/projects docker-compose-sources
|
docker-compose-runtest: awx/projects docker-compose-sources
|
||||||
docker-compose -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports awx_1 /start_tests.sh
|
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports awx_1 /start_tests.sh
|
||||||
|
|
||||||
docker-compose-build-swagger: awx/projects docker-compose-sources
|
docker-compose-build-swagger: awx/projects docker-compose-sources
|
||||||
docker-compose -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports --no-deps awx_1 /start_tests.sh swagger
|
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports --no-deps awx_1 /start_tests.sh swagger
|
||||||
|
|
||||||
SCHEMA_DIFF_BASE_BRANCH ?= devel
|
SCHEMA_DIFF_BASE_BRANCH ?= devel
|
||||||
detect-schema-change: genschema
|
detect-schema-change: genschema
|
||||||
@@ -504,7 +520,7 @@ detect-schema-change: genschema
|
|||||||
diff -u -b reference-schema.json schema.json
|
diff -u -b reference-schema.json schema.json
|
||||||
|
|
||||||
docker-compose-clean: awx/projects
|
docker-compose-clean: awx/projects
|
||||||
docker-compose -f tools/docker-compose/_sources/docker-compose.yml rm -sf
|
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml rm -sf
|
||||||
|
|
||||||
docker-compose-container-group-clean:
|
docker-compose-container-group-clean:
|
||||||
@if [ -f "tools/docker-compose-minikube/_sources/minikube" ]; then \
|
@if [ -f "tools/docker-compose-minikube/_sources/minikube" ]; then \
|
||||||
@@ -532,10 +548,10 @@ docker-refresh: docker-clean docker-compose
|
|||||||
|
|
||||||
## Docker Development Environment with Elastic Stack Connected
|
## Docker Development Environment with Elastic Stack Connected
|
||||||
docker-compose-elk: awx/projects docker-compose-sources
|
docker-compose-elk: awx/projects docker-compose-sources
|
||||||
docker-compose -f tools/docker-compose/_sources/docker-compose.yml -f tools/elastic/docker-compose.logstash-link.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate
|
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml -f tools/elastic/docker-compose.logstash-link.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate
|
||||||
|
|
||||||
docker-compose-cluster-elk: awx/projects docker-compose-sources
|
docker-compose-cluster-elk: awx/projects docker-compose-sources
|
||||||
docker-compose -f tools/docker-compose/_sources/docker-compose.yml -f tools/elastic/docker-compose.logstash-link-cluster.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate
|
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml -f tools/elastic/docker-compose.logstash-link-cluster.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate
|
||||||
|
|
||||||
docker-compose-container-group:
|
docker-compose-container-group:
|
||||||
MINIKUBE_CONTAINER_GROUP=true make docker-compose
|
MINIKUBE_CONTAINER_GROUP=true make docker-compose
|
||||||
@@ -593,13 +609,12 @@ pot: $(UI_BUILD_FLAG_FILE)
|
|||||||
po: $(UI_BUILD_FLAG_FILE)
|
po: $(UI_BUILD_FLAG_FILE)
|
||||||
$(NPM_BIN) --prefix awx/ui --loglevel warn run extract-strings -- --clean
|
$(NPM_BIN) --prefix awx/ui --loglevel warn run extract-strings -- --clean
|
||||||
|
|
||||||
LANG = "en_us"
|
|
||||||
## generate API django .pot .po
|
## generate API django .pot .po
|
||||||
messages:
|
messages:
|
||||||
@if [ "$(VENV_BASE)" ]; then \
|
@if [ "$(VENV_BASE)" ]; then \
|
||||||
. $(VENV_BASE)/awx/bin/activate; \
|
. $(VENV_BASE)/awx/bin/activate; \
|
||||||
fi; \
|
fi; \
|
||||||
$(PYTHON) manage.py makemessages -l $(LANG) --keep-pot
|
$(PYTHON) manage.py makemessages -l en_us --keep-pot
|
||||||
|
|
||||||
print-%:
|
print-%:
|
||||||
@echo $($*)
|
@echo $($*)
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ else:
|
|||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
|
||||||
if HAS_DJANGO is True:
|
if HAS_DJANGO is True:
|
||||||
|
|
||||||
# See upgrade blocker note in requirements/README.md
|
# See upgrade blocker note in requirements/README.md
|
||||||
try:
|
try:
|
||||||
names_digest('foo', 'bar', 'baz', length=8)
|
names_digest('foo', 'bar', 'baz', length=8)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# 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
|
||||||
@@ -9,6 +8,7 @@ from rest_framework import serializers
|
|||||||
from awx.conf import fields, register, register_validate
|
from awx.conf import fields, register, register_validate
|
||||||
from awx.api.fields import OAuth2ProviderField
|
from awx.api.fields import OAuth2ProviderField
|
||||||
from oauth2_provider.settings import oauth2_settings
|
from oauth2_provider.settings import oauth2_settings
|
||||||
|
from awx.sso.common import is_remote_auth_enabled
|
||||||
|
|
||||||
|
|
||||||
register(
|
register(
|
||||||
@@ -96,22 +96,20 @@ 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):
|
||||||
remote_auth_settings = [
|
if attrs.get('DISABLE_LOCAL_AUTH', False) and not is_remote_auth_enabled():
|
||||||
'AUTH_LDAP_SERVER_URI',
|
raise serializers.ValidationError(_("There are no remote authentication systems configured."))
|
||||||
'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY',
|
|
||||||
'SOCIAL_AUTH_GITHUB_KEY',
|
|
||||||
'SOCIAL_AUTH_GITHUB_ORG_KEY',
|
|
||||||
'SOCIAL_AUTH_GITHUB_TEAM_KEY',
|
|
||||||
'SOCIAL_AUTH_SAML_ENABLED_IDPS',
|
|
||||||
'RADIUS_SERVER',
|
|
||||||
'TACACSPLUS_HOST',
|
|
||||||
]
|
|
||||||
if attrs.get('DISABLE_LOCAL_AUTH', False):
|
|
||||||
if not any(getattr(settings, s, None) for s in remote_auth_settings):
|
|
||||||
raise serializers.ValidationError(_("There are no remote authentication systems configured."))
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ class VerbatimField(serializers.Field):
|
|||||||
|
|
||||||
|
|
||||||
class OAuth2ProviderField(fields.DictField):
|
class OAuth2ProviderField(fields.DictField):
|
||||||
|
|
||||||
default_error_messages = {'invalid_key_names': _('Invalid key names: {invalid_key_names}')}
|
default_error_messages = {'invalid_key_names': _('Invalid key names: {invalid_key_names}')}
|
||||||
valid_key_names = {'ACCESS_TOKEN_EXPIRE_SECONDS', 'AUTHORIZATION_CODE_EXPIRE_SECONDS', 'REFRESH_TOKEN_EXPIRE_SECONDS'}
|
valid_key_names = {'ACCESS_TOKEN_EXPIRE_SECONDS', 'AUTHORIZATION_CODE_EXPIRE_SECONDS', 'REFRESH_TOKEN_EXPIRE_SECONDS'}
|
||||||
child = fields.IntegerField(min_value=1)
|
child = fields.IntegerField(min_value=1)
|
||||||
|
|||||||
@@ -155,12 +155,11 @@ class FieldLookupBackend(BaseFilterBackend):
|
|||||||
'search',
|
'search',
|
||||||
)
|
)
|
||||||
|
|
||||||
# A list of fields that we know can be filtered on without the possiblity
|
# A list of fields that we know can be filtered on without the possibility
|
||||||
# of introducing duplicates
|
# of introducing duplicates
|
||||||
NO_DUPLICATES_ALLOW_LIST = (CharField, IntegerField, BooleanField, TextField)
|
NO_DUPLICATES_ALLOW_LIST = (CharField, IntegerField, BooleanField, TextField)
|
||||||
|
|
||||||
def get_fields_from_lookup(self, model, lookup):
|
def get_fields_from_lookup(self, model, lookup):
|
||||||
|
|
||||||
if '__' in lookup and lookup.rsplit('__', 1)[-1] in self.SUPPORTED_LOOKUPS:
|
if '__' in lookup and lookup.rsplit('__', 1)[-1] in self.SUPPORTED_LOOKUPS:
|
||||||
path, suffix = lookup.rsplit('__', 1)
|
path, suffix = lookup.rsplit('__', 1)
|
||||||
else:
|
else:
|
||||||
@@ -269,7 +268,7 @@ class FieldLookupBackend(BaseFilterBackend):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# HACK: make `created` available via API for the Django User ORM model
|
# HACK: make `created` available via API for the Django User ORM model
|
||||||
# so it keep compatiblity with other objects which exposes the `created` attr.
|
# so it keep compatibility with other objects which exposes the `created` attr.
|
||||||
if queryset.model._meta.object_name == 'User' and key.startswith('created'):
|
if queryset.model._meta.object_name == 'User' and key.startswith('created'):
|
||||||
key = key.replace('created', 'date_joined')
|
key = key.replace('created', 'date_joined')
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from rest_framework import generics
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.renderers import StaticHTMLRenderer
|
from rest_framework.renderers import StaticHTMLRenderer
|
||||||
from rest_framework.negotiation import DefaultContentNegotiation
|
from rest_framework.negotiation import DefaultContentNegotiation
|
||||||
|
|
||||||
@@ -135,7 +135,6 @@ def get_default_schema():
|
|||||||
|
|
||||||
|
|
||||||
class APIView(views.APIView):
|
class APIView(views.APIView):
|
||||||
|
|
||||||
schema = get_default_schema()
|
schema = get_default_schema()
|
||||||
versioning_class = URLPathVersioning
|
versioning_class = URLPathVersioning
|
||||||
|
|
||||||
@@ -675,7 +674,7 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
|||||||
location = None
|
location = None
|
||||||
created = True
|
created = True
|
||||||
|
|
||||||
# Retrive the sub object (whether created or by ID).
|
# Retrieve the sub object (whether created or by ID).
|
||||||
sub = get_object_or_400(self.model, pk=sub_id)
|
sub = get_object_or_400(self.model, pk=sub_id)
|
||||||
|
|
||||||
# Verify we have permission to attach.
|
# Verify we have permission to attach.
|
||||||
@@ -800,7 +799,6 @@ class RetrieveUpdateDestroyAPIView(RetrieveUpdateAPIView, DestroyAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class ResourceAccessList(ParentMixin, ListAPIView):
|
class ResourceAccessList(ParentMixin, ListAPIView):
|
||||||
|
|
||||||
serializer_class = ResourceAccessListElementSerializer
|
serializer_class = ResourceAccessListElementSerializer
|
||||||
ordering = ('username',)
|
ordering = ('username',)
|
||||||
|
|
||||||
@@ -823,9 +821,8 @@ def trigger_delayed_deep_copy(*args, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
class CopyAPIView(GenericAPIView):
|
class CopyAPIView(GenericAPIView):
|
||||||
|
|
||||||
serializer_class = CopySerializer
|
serializer_class = CopySerializer
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (IsAuthenticated,)
|
||||||
copy_return_serializer_class = None
|
copy_return_serializer_class = None
|
||||||
new_in_330 = True
|
new_in_330 = True
|
||||||
new_in_api_v2 = True
|
new_in_api_v2 = True
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ class Metadata(metadata.SimpleMetadata):
|
|||||||
# Special handling of notification configuration where the required properties
|
# Special handling of notification configuration where the required properties
|
||||||
# are conditional on the type selected.
|
# are conditional on the type selected.
|
||||||
if field.field_name == 'notification_configuration':
|
if field.field_name == 'notification_configuration':
|
||||||
for (notification_type_name, notification_tr_name, notification_type_class) in NotificationTemplate.NOTIFICATION_TYPES:
|
for notification_type_name, notification_tr_name, notification_type_class in NotificationTemplate.NOTIFICATION_TYPES:
|
||||||
field_info[notification_type_name] = notification_type_class.init_parameters
|
field_info[notification_type_name] = notification_type_class.init_parameters
|
||||||
|
|
||||||
# Special handling of notification messages where the required properties
|
# Special handling of notification messages where the required properties
|
||||||
@@ -138,7 +138,7 @@ class Metadata(metadata.SimpleMetadata):
|
|||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
view_model = None
|
view_model = None
|
||||||
if view_model == NotificationTemplate and field.field_name == 'messages':
|
if view_model == NotificationTemplate and field.field_name == 'messages':
|
||||||
for (notification_type_name, notification_tr_name, notification_type_class) in NotificationTemplate.NOTIFICATION_TYPES:
|
for notification_type_name, notification_tr_name, notification_type_class in NotificationTemplate.NOTIFICATION_TYPES:
|
||||||
field_info[notification_type_name] = notification_type_class.default_messages
|
field_info[notification_type_name] = notification_type_class.default_messages
|
||||||
|
|
||||||
# Update type of fields returned...
|
# Update type of fields returned...
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class DisabledPaginator(DjangoPaginator):
|
|||||||
|
|
||||||
|
|
||||||
class Pagination(pagination.PageNumberPagination):
|
class Pagination(pagination.PageNumberPagination):
|
||||||
|
|
||||||
page_size_query_param = 'page_size'
|
page_size_query_param = 'page_size'
|
||||||
max_page_size = settings.MAX_PAGE_SIZE
|
max_page_size = settings.MAX_PAGE_SIZE
|
||||||
count_disabled = False
|
count_disabled = False
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ class SurrogateEncoder(encoders.JSONEncoder):
|
|||||||
|
|
||||||
|
|
||||||
class DefaultJSONRenderer(renderers.JSONRenderer):
|
class DefaultJSONRenderer(renderers.JSONRenderer):
|
||||||
|
|
||||||
encoder_class = SurrogateEncoder
|
encoder_class = SurrogateEncoder
|
||||||
|
|
||||||
|
|
||||||
@@ -61,7 +60,7 @@ class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
|
|||||||
delattr(renderer_context['view'], '_request')
|
delattr(renderer_context['view'], '_request')
|
||||||
|
|
||||||
def get_raw_data_form(self, data, view, method, request):
|
def get_raw_data_form(self, data, view, method, request):
|
||||||
# Set a flag on the view to indiciate to the view/serializer that we're
|
# Set a flag on the view to indicate to the view/serializer that we're
|
||||||
# creating a raw data form for the browsable API. Store the original
|
# creating a raw data form for the browsable API. Store the original
|
||||||
# request method to determine how to populate the raw data form.
|
# request method to determine how to populate the raw data form.
|
||||||
if request.method in {'OPTIONS', 'DELETE'}:
|
if request.method in {'OPTIONS', 'DELETE'}:
|
||||||
@@ -95,7 +94,6 @@ class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
|
|||||||
|
|
||||||
|
|
||||||
class PlainTextRenderer(renderers.BaseRenderer):
|
class PlainTextRenderer(renderers.BaseRenderer):
|
||||||
|
|
||||||
media_type = 'text/plain'
|
media_type = 'text/plain'
|
||||||
format = 'txt'
|
format = 'txt'
|
||||||
|
|
||||||
@@ -106,18 +104,15 @@ class PlainTextRenderer(renderers.BaseRenderer):
|
|||||||
|
|
||||||
|
|
||||||
class DownloadTextRenderer(PlainTextRenderer):
|
class DownloadTextRenderer(PlainTextRenderer):
|
||||||
|
|
||||||
format = "txt_download"
|
format = "txt_download"
|
||||||
|
|
||||||
|
|
||||||
class AnsiTextRenderer(PlainTextRenderer):
|
class AnsiTextRenderer(PlainTextRenderer):
|
||||||
|
|
||||||
media_type = 'text/plain'
|
media_type = 'text/plain'
|
||||||
format = 'ansi'
|
format = 'ansi'
|
||||||
|
|
||||||
|
|
||||||
class AnsiDownloadRenderer(PlainTextRenderer):
|
class AnsiDownloadRenderer(PlainTextRenderer):
|
||||||
|
|
||||||
format = "ansi_download"
|
format = "ansi_download"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -108,12 +108,11 @@ from awx.main.utils import (
|
|||||||
extract_ansible_vars,
|
extract_ansible_vars,
|
||||||
encrypt_dict,
|
encrypt_dict,
|
||||||
prefetch_page_capabilities,
|
prefetch_page_capabilities,
|
||||||
get_external_account,
|
|
||||||
truncate_stdout,
|
truncate_stdout,
|
||||||
)
|
)
|
||||||
from awx.main.utils.filters import SmartFilter
|
from awx.main.utils.filters import SmartFilter
|
||||||
from awx.main.utils.named_url_graph import reset_counters
|
from awx.main.utils.named_url_graph import reset_counters
|
||||||
from awx.main.scheduler.task_manager_models import TaskManagerInstanceGroups, TaskManagerInstances
|
from awx.main.scheduler.task_manager_models import TaskManagerModels
|
||||||
from awx.main.redact import UriCleaner, REPLACE_STR
|
from awx.main.redact import UriCleaner, REPLACE_STR
|
||||||
|
|
||||||
from awx.main.validators import vars_validate_or_raise
|
from awx.main.validators import vars_validate_or_raise
|
||||||
@@ -124,6 +123,8 @@ from awx.api.fields import BooleanNullField, CharNullField, ChoiceNullField, Ver
|
|||||||
# AWX Utils
|
# AWX Utils
|
||||||
from awx.api.validators import HostnameRegexValidator
|
from awx.api.validators import HostnameRegexValidator
|
||||||
|
|
||||||
|
from awx.sso.common import get_external_account
|
||||||
|
|
||||||
logger = logging.getLogger('awx.api.serializers')
|
logger = logging.getLogger('awx.api.serializers')
|
||||||
|
|
||||||
# Fields that should be summarized regardless of object type.
|
# Fields that should be summarized regardless of object type.
|
||||||
@@ -200,7 +201,6 @@ def reverse_gfk(content_object, request):
|
|||||||
|
|
||||||
|
|
||||||
class CopySerializer(serializers.Serializer):
|
class CopySerializer(serializers.Serializer):
|
||||||
|
|
||||||
name = serializers.CharField()
|
name = serializers.CharField()
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
@@ -432,7 +432,6 @@ class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetacl
|
|||||||
continue
|
continue
|
||||||
summary_fields[fk] = OrderedDict()
|
summary_fields[fk] = OrderedDict()
|
||||||
for field in related_fields:
|
for field in related_fields:
|
||||||
|
|
||||||
fval = getattr(fkval, field, None)
|
fval = getattr(fkval, field, None)
|
||||||
|
|
||||||
if fval is None and field == 'type':
|
if fval is None and field == 'type':
|
||||||
@@ -538,7 +537,7 @@ class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetacl
|
|||||||
#
|
#
|
||||||
# This logic is to force rendering choice's on an uneditable field.
|
# This logic is to force rendering choice's on an uneditable field.
|
||||||
# Note: Consider expanding this rendering for more than just choices fields
|
# Note: Consider expanding this rendering for more than just choices fields
|
||||||
# Note: This logic works in conjuction with
|
# Note: This logic works in conjunction with
|
||||||
if hasattr(model_field, 'choices') and model_field.choices:
|
if hasattr(model_field, 'choices') and model_field.choices:
|
||||||
was_editable = model_field.editable
|
was_editable = model_field.editable
|
||||||
model_field.editable = True
|
model_field.editable = True
|
||||||
@@ -930,7 +929,6 @@ class UnifiedJobListSerializer(UnifiedJobSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class UnifiedJobStdoutSerializer(UnifiedJobSerializer):
|
class UnifiedJobStdoutSerializer(UnifiedJobSerializer):
|
||||||
|
|
||||||
result_stdout = serializers.SerializerMethodField()
|
result_stdout = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -944,7 +942,6 @@ class UnifiedJobStdoutSerializer(UnifiedJobSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class UserSerializer(BaseSerializer):
|
class UserSerializer(BaseSerializer):
|
||||||
|
|
||||||
password = serializers.CharField(required=False, default='', write_only=True, help_text=_('Write-only field used to change the password.'))
|
password = serializers.CharField(required=False, default='', write_only=True, help_text=_('Write-only field used to change the password.'))
|
||||||
ldap_dn = serializers.CharField(source='profile.ldap_dn', read_only=True)
|
ldap_dn = serializers.CharField(source='profile.ldap_dn', read_only=True)
|
||||||
external_account = serializers.SerializerMethodField(help_text=_('Set if the account is managed by an external service'))
|
external_account = serializers.SerializerMethodField(help_text=_('Set if the account is managed by an external service'))
|
||||||
@@ -991,23 +988,8 @@ class UserSerializer(BaseSerializer):
|
|||||||
def _update_password(self, obj, new_password):
|
def _update_password(self, obj, new_password):
|
||||||
# For now we're not raising an error, just not saving password for
|
# For now we're not raising an error, just not saving password for
|
||||||
# users managed by LDAP who already have an unusable password set.
|
# users managed by LDAP who already have an unusable password set.
|
||||||
if getattr(settings, 'AUTH_LDAP_SERVER_URI', None):
|
# Get external password will return something like ldap or enterprise or None if the user isn't external. We only want to allow a password update for a None option
|
||||||
try:
|
if new_password and not self.get_external_account(obj):
|
||||||
if obj.pk and obj.profile.ldap_dn and not obj.has_usable_password():
|
|
||||||
new_password = None
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
if (
|
|
||||||
getattr(settings, 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', None)
|
|
||||||
or getattr(settings, 'SOCIAL_AUTH_GITHUB_KEY', None)
|
|
||||||
or getattr(settings, 'SOCIAL_AUTH_GITHUB_ORG_KEY', None)
|
|
||||||
or getattr(settings, 'SOCIAL_AUTH_GITHUB_TEAM_KEY', None)
|
|
||||||
or getattr(settings, 'SOCIAL_AUTH_SAML_ENABLED_IDPS', None)
|
|
||||||
) and obj.social_auth.all():
|
|
||||||
new_password = None
|
|
||||||
if (getattr(settings, 'RADIUS_SERVER', None) or getattr(settings, 'TACACSPLUS_HOST', None)) and obj.enterprise_auth.all():
|
|
||||||
new_password = None
|
|
||||||
if new_password:
|
|
||||||
obj.set_password(new_password)
|
obj.set_password(new_password)
|
||||||
obj.save(update_fields=['password'])
|
obj.save(update_fields=['password'])
|
||||||
|
|
||||||
@@ -1104,7 +1086,6 @@ class UserActivityStreamSerializer(UserSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class BaseOAuth2TokenSerializer(BaseSerializer):
|
class BaseOAuth2TokenSerializer(BaseSerializer):
|
||||||
|
|
||||||
refresh_token = serializers.SerializerMethodField()
|
refresh_token = serializers.SerializerMethodField()
|
||||||
token = serializers.SerializerMethodField()
|
token = serializers.SerializerMethodField()
|
||||||
ALLOWED_SCOPES = ['read', 'write']
|
ALLOWED_SCOPES = ['read', 'write']
|
||||||
@@ -1222,7 +1203,6 @@ class UserPersonalTokenSerializer(BaseOAuth2TokenSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class OAuth2ApplicationSerializer(BaseSerializer):
|
class OAuth2ApplicationSerializer(BaseSerializer):
|
||||||
|
|
||||||
show_capabilities = ['edit', 'delete']
|
show_capabilities = ['edit', 'delete']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -1457,7 +1437,6 @@ class ExecutionEnvironmentSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
|
class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
|
||||||
|
|
||||||
status = serializers.ChoiceField(choices=Project.PROJECT_STATUS_CHOICES, read_only=True)
|
status = serializers.ChoiceField(choices=Project.PROJECT_STATUS_CHOICES, read_only=True)
|
||||||
last_update_failed = serializers.BooleanField(read_only=True)
|
last_update_failed = serializers.BooleanField(read_only=True)
|
||||||
last_updated = serializers.DateTimeField(read_only=True)
|
last_updated = serializers.DateTimeField(read_only=True)
|
||||||
@@ -1548,7 +1527,6 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectPlaybooksSerializer(ProjectSerializer):
|
class ProjectPlaybooksSerializer(ProjectSerializer):
|
||||||
|
|
||||||
playbooks = serializers.SerializerMethodField(help_text=_('Array of playbooks available within this project.'))
|
playbooks = serializers.SerializerMethodField(help_text=_('Array of playbooks available within this project.'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -1566,7 +1544,6 @@ class ProjectPlaybooksSerializer(ProjectSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectInventoriesSerializer(ProjectSerializer):
|
class ProjectInventoriesSerializer(ProjectSerializer):
|
||||||
|
|
||||||
inventory_files = serializers.ReadOnlyField(help_text=_('Array of inventory files and directories available within this project, ' 'not comprehensive.'))
|
inventory_files = serializers.ReadOnlyField(help_text=_('Array of inventory files and directories available within this project, ' 'not comprehensive.'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -1581,7 +1558,6 @@ class ProjectInventoriesSerializer(ProjectSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectUpdateViewSerializer(ProjectSerializer):
|
class ProjectUpdateViewSerializer(ProjectSerializer):
|
||||||
|
|
||||||
can_update = serializers.BooleanField(read_only=True)
|
can_update = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -1611,7 +1587,6 @@ class ProjectUpdateSerializer(UnifiedJobSerializer, ProjectOptionsSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectUpdateDetailSerializer(ProjectUpdateSerializer):
|
class ProjectUpdateDetailSerializer(ProjectUpdateSerializer):
|
||||||
|
|
||||||
playbook_counts = serializers.SerializerMethodField(help_text=_('A count of all plays and tasks for the job run.'))
|
playbook_counts = serializers.SerializerMethodField(help_text=_('A count of all plays and tasks for the job run.'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -1634,7 +1609,6 @@ class ProjectUpdateListSerializer(ProjectUpdateSerializer, UnifiedJobListSeriali
|
|||||||
|
|
||||||
|
|
||||||
class ProjectUpdateCancelSerializer(ProjectUpdateSerializer):
|
class ProjectUpdateCancelSerializer(ProjectUpdateSerializer):
|
||||||
|
|
||||||
can_cancel = serializers.BooleanField(read_only=True)
|
can_cancel = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -1972,7 +1946,6 @@ class GroupSerializer(BaseSerializerWithVariables):
|
|||||||
|
|
||||||
|
|
||||||
class GroupTreeSerializer(GroupSerializer):
|
class GroupTreeSerializer(GroupSerializer):
|
||||||
|
|
||||||
children = serializers.SerializerMethodField()
|
children = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -2070,7 +2043,6 @@ class InventorySourceOptionsSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOptionsSerializer):
|
class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOptionsSerializer):
|
||||||
|
|
||||||
status = serializers.ChoiceField(choices=InventorySource.INVENTORY_SOURCE_STATUS_CHOICES, read_only=True)
|
status = serializers.ChoiceField(choices=InventorySource.INVENTORY_SOURCE_STATUS_CHOICES, read_only=True)
|
||||||
last_update_failed = serializers.BooleanField(read_only=True)
|
last_update_failed = serializers.BooleanField(read_only=True)
|
||||||
last_updated = serializers.DateTimeField(read_only=True)
|
last_updated = serializers.DateTimeField(read_only=True)
|
||||||
@@ -2215,7 +2187,6 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
|||||||
|
|
||||||
|
|
||||||
class InventorySourceUpdateSerializer(InventorySourceSerializer):
|
class InventorySourceUpdateSerializer(InventorySourceSerializer):
|
||||||
|
|
||||||
can_update = serializers.BooleanField(read_only=True)
|
can_update = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -2232,7 +2203,6 @@ class InventorySourceUpdateSerializer(InventorySourceSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSerializer):
|
class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSerializer):
|
||||||
|
|
||||||
custom_virtualenv = serializers.ReadOnlyField()
|
custom_virtualenv = serializers.ReadOnlyField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -2273,7 +2243,6 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri
|
|||||||
|
|
||||||
|
|
||||||
class InventoryUpdateDetailSerializer(InventoryUpdateSerializer):
|
class InventoryUpdateDetailSerializer(InventoryUpdateSerializer):
|
||||||
|
|
||||||
source_project = serializers.SerializerMethodField(help_text=_('The project used for this job.'), method_name='get_source_project_id')
|
source_project = serializers.SerializerMethodField(help_text=_('The project used for this job.'), method_name='get_source_project_id')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -2324,7 +2293,6 @@ class InventoryUpdateListSerializer(InventoryUpdateSerializer, UnifiedJobListSer
|
|||||||
|
|
||||||
|
|
||||||
class InventoryUpdateCancelSerializer(InventoryUpdateSerializer):
|
class InventoryUpdateCancelSerializer(InventoryUpdateSerializer):
|
||||||
|
|
||||||
can_cancel = serializers.BooleanField(read_only=True)
|
can_cancel = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -2682,7 +2650,6 @@ class CredentialSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class CredentialSerializerCreate(CredentialSerializer):
|
class CredentialSerializerCreate(CredentialSerializer):
|
||||||
|
|
||||||
user = serializers.PrimaryKeyRelatedField(
|
user = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=User.objects.all(),
|
queryset=User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@@ -3037,7 +3004,6 @@ class JobTemplateWithSpecSerializer(JobTemplateSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||||
|
|
||||||
passwords_needed_to_start = serializers.ReadOnlyField()
|
passwords_needed_to_start = serializers.ReadOnlyField()
|
||||||
artifacts = serializers.SerializerMethodField()
|
artifacts = serializers.SerializerMethodField()
|
||||||
|
|
||||||
@@ -3120,7 +3086,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class JobDetailSerializer(JobSerializer):
|
class JobDetailSerializer(JobSerializer):
|
||||||
|
|
||||||
playbook_counts = serializers.SerializerMethodField(help_text=_('A count of all plays and tasks for the job run.'))
|
playbook_counts = serializers.SerializerMethodField(help_text=_('A count of all plays and tasks for the job run.'))
|
||||||
custom_virtualenv = serializers.ReadOnlyField()
|
custom_virtualenv = serializers.ReadOnlyField()
|
||||||
|
|
||||||
@@ -3138,7 +3103,6 @@ class JobDetailSerializer(JobSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class JobCancelSerializer(BaseSerializer):
|
class JobCancelSerializer(BaseSerializer):
|
||||||
|
|
||||||
can_cancel = serializers.BooleanField(read_only=True)
|
can_cancel = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -3147,7 +3111,6 @@ class JobCancelSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class JobRelaunchSerializer(BaseSerializer):
|
class JobRelaunchSerializer(BaseSerializer):
|
||||||
|
|
||||||
passwords_needed_to_start = serializers.SerializerMethodField()
|
passwords_needed_to_start = serializers.SerializerMethodField()
|
||||||
retry_counts = serializers.SerializerMethodField()
|
retry_counts = serializers.SerializerMethodField()
|
||||||
hosts = serializers.ChoiceField(
|
hosts = serializers.ChoiceField(
|
||||||
@@ -3207,7 +3170,6 @@ class JobRelaunchSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class JobCreateScheduleSerializer(LabelsListMixin, BaseSerializer):
|
class JobCreateScheduleSerializer(LabelsListMixin, BaseSerializer):
|
||||||
|
|
||||||
can_schedule = serializers.SerializerMethodField()
|
can_schedule = serializers.SerializerMethodField()
|
||||||
prompts = serializers.SerializerMethodField()
|
prompts = serializers.SerializerMethodField()
|
||||||
|
|
||||||
@@ -3333,7 +3295,6 @@ class AdHocCommandDetailSerializer(AdHocCommandSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class AdHocCommandCancelSerializer(AdHocCommandSerializer):
|
class AdHocCommandCancelSerializer(AdHocCommandSerializer):
|
||||||
|
|
||||||
can_cancel = serializers.BooleanField(read_only=True)
|
can_cancel = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -3372,7 +3333,6 @@ class SystemJobTemplateSerializer(UnifiedJobTemplateSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class SystemJobSerializer(UnifiedJobSerializer):
|
class SystemJobSerializer(UnifiedJobSerializer):
|
||||||
|
|
||||||
result_stdout = serializers.SerializerMethodField()
|
result_stdout = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -3399,7 +3359,6 @@ class SystemJobSerializer(UnifiedJobSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class SystemJobCancelSerializer(SystemJobSerializer):
|
class SystemJobCancelSerializer(SystemJobSerializer):
|
||||||
|
|
||||||
can_cancel = serializers.BooleanField(read_only=True)
|
can_cancel = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -3564,7 +3523,6 @@ class WorkflowJobListSerializer(WorkflowJobSerializer, UnifiedJobListSerializer)
|
|||||||
|
|
||||||
|
|
||||||
class WorkflowJobCancelSerializer(WorkflowJobSerializer):
|
class WorkflowJobCancelSerializer(WorkflowJobSerializer):
|
||||||
|
|
||||||
can_cancel = serializers.BooleanField(read_only=True)
|
can_cancel = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -3578,7 +3536,6 @@ class WorkflowApprovalViewSerializer(UnifiedJobSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class WorkflowApprovalSerializer(UnifiedJobSerializer):
|
class WorkflowApprovalSerializer(UnifiedJobSerializer):
|
||||||
|
|
||||||
can_approve_or_deny = serializers.SerializerMethodField()
|
can_approve_or_deny = serializers.SerializerMethodField()
|
||||||
approval_expiration = serializers.SerializerMethodField()
|
approval_expiration = serializers.SerializerMethodField()
|
||||||
timed_out = serializers.ReadOnlyField()
|
timed_out = serializers.ReadOnlyField()
|
||||||
@@ -3973,7 +3930,6 @@ class JobHostSummarySerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class JobEventSerializer(BaseSerializer):
|
class JobEventSerializer(BaseSerializer):
|
||||||
|
|
||||||
event_display = serializers.CharField(source='get_event_display2', read_only=True)
|
event_display = serializers.CharField(source='get_event_display2', read_only=True)
|
||||||
event_level = serializers.IntegerField(read_only=True)
|
event_level = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@@ -4027,7 +3983,7 @@ class JobEventSerializer(BaseSerializer):
|
|||||||
# Show full stdout for playbook_on_* events.
|
# Show full stdout for playbook_on_* events.
|
||||||
if obj and obj.event.startswith('playbook_on'):
|
if obj and obj.event.startswith('playbook_on'):
|
||||||
return data
|
return data
|
||||||
# If the view logic says to not trunctate (request was to the detail view or a param was used)
|
# If the view logic says to not truncate (request was to the detail view or a param was used)
|
||||||
if self.context.get('no_truncate', False):
|
if self.context.get('no_truncate', False):
|
||||||
return data
|
return data
|
||||||
max_bytes = settings.EVENT_STDOUT_MAX_BYTES_DISPLAY
|
max_bytes = settings.EVENT_STDOUT_MAX_BYTES_DISPLAY
|
||||||
@@ -4058,7 +4014,7 @@ class ProjectUpdateEventSerializer(JobEventSerializer):
|
|||||||
# raw SCM URLs in their stdout (which *could* contain passwords)
|
# raw SCM URLs in their stdout (which *could* contain passwords)
|
||||||
# attempt to detect and filter HTTP basic auth passwords in the stdout
|
# attempt to detect and filter HTTP basic auth passwords in the stdout
|
||||||
# of these types of events
|
# of these types of events
|
||||||
if obj.event_data.get('task_action') in ('git', 'svn'):
|
if obj.event_data.get('task_action') in ('git', 'svn', 'ansible.builtin.git', 'ansible.builtin.svn'):
|
||||||
try:
|
try:
|
||||||
return json.loads(UriCleaner.remove_sensitive(json.dumps(obj.event_data)))
|
return json.loads(UriCleaner.remove_sensitive(json.dumps(obj.event_data)))
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -4069,7 +4025,6 @@ class ProjectUpdateEventSerializer(JobEventSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class AdHocCommandEventSerializer(BaseSerializer):
|
class AdHocCommandEventSerializer(BaseSerializer):
|
||||||
|
|
||||||
event_display = serializers.CharField(source='get_event_display', read_only=True)
|
event_display = serializers.CharField(source='get_event_display', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -4103,7 +4058,7 @@ class AdHocCommandEventSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
data = super(AdHocCommandEventSerializer, self).to_representation(obj)
|
data = super(AdHocCommandEventSerializer, self).to_representation(obj)
|
||||||
# If the view logic says to not trunctate (request was to the detail view or a param was used)
|
# If the view logic says to not truncate (request was to the detail view or a param was used)
|
||||||
if self.context.get('no_truncate', False):
|
if self.context.get('no_truncate', False):
|
||||||
return data
|
return data
|
||||||
max_bytes = settings.EVENT_STDOUT_MAX_BYTES_DISPLAY
|
max_bytes = settings.EVENT_STDOUT_MAX_BYTES_DISPLAY
|
||||||
@@ -4351,7 +4306,6 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class WorkflowJobLaunchSerializer(BaseSerializer):
|
class WorkflowJobLaunchSerializer(BaseSerializer):
|
||||||
|
|
||||||
can_start_without_user_input = serializers.BooleanField(read_only=True)
|
can_start_without_user_input = serializers.BooleanField(read_only=True)
|
||||||
defaults = serializers.SerializerMethodField()
|
defaults = serializers.SerializerMethodField()
|
||||||
variables_needed_to_start = serializers.ReadOnlyField()
|
variables_needed_to_start = serializers.ReadOnlyField()
|
||||||
@@ -4408,7 +4362,6 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def get_defaults(self, obj):
|
def get_defaults(self, obj):
|
||||||
|
|
||||||
defaults_dict = {}
|
defaults_dict = {}
|
||||||
for field_name in WorkflowJobTemplate.get_ask_mapping().keys():
|
for field_name in WorkflowJobTemplate.get_ask_mapping().keys():
|
||||||
if field_name == 'inventory':
|
if field_name == 'inventory':
|
||||||
@@ -4425,7 +4378,6 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
|
|||||||
return dict(name=obj.name, id=obj.id, description=obj.description)
|
return dict(name=obj.name, id=obj.id, description=obj.description)
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
|
|
||||||
template = self.instance
|
template = self.instance
|
||||||
|
|
||||||
accepted, rejected, errors = template._accept_or_ignore_job_kwargs(**attrs)
|
accepted, rejected, errors = template._accept_or_ignore_job_kwargs(**attrs)
|
||||||
@@ -4666,7 +4618,6 @@ class NotificationTemplateSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class NotificationSerializer(BaseSerializer):
|
class NotificationSerializer(BaseSerializer):
|
||||||
|
|
||||||
body = serializers.SerializerMethodField(help_text=_('Notification body'))
|
body = serializers.SerializerMethodField(help_text=_('Notification body'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -4800,7 +4751,7 @@ class ScheduleSerializer(LaunchConfigurationBaseSerializer, SchedulePreviewSeria
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
until = serializers.SerializerMethodField(
|
until = serializers.SerializerMethodField(
|
||||||
help_text=_('The date this schedule will end. This field is computed from the RRULE. If the schedule does not end an emptry string will be returned'),
|
help_text=_('The date this schedule will end. This field is computed from the RRULE. If the schedule does not end an empty string will be returned'),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -5038,14 +4989,11 @@ class InstanceHealthCheckSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class InstanceGroupSerializer(BaseSerializer):
|
class InstanceGroupSerializer(BaseSerializer):
|
||||||
|
|
||||||
show_capabilities = ['edit', 'delete']
|
show_capabilities = ['edit', 'delete']
|
||||||
|
capacity = serializers.SerializerMethodField()
|
||||||
consumed_capacity = serializers.SerializerMethodField()
|
consumed_capacity = serializers.SerializerMethodField()
|
||||||
percent_capacity_remaining = serializers.SerializerMethodField()
|
percent_capacity_remaining = serializers.SerializerMethodField()
|
||||||
jobs_running = serializers.IntegerField(
|
jobs_running = serializers.SerializerMethodField()
|
||||||
help_text=_('Count of jobs in the running or waiting state that ' 'are targeted for this instance group'), read_only=True
|
|
||||||
)
|
|
||||||
jobs_total = serializers.IntegerField(help_text=_('Count of all jobs that target this instance group'), read_only=True)
|
jobs_total = serializers.IntegerField(help_text=_('Count of all jobs that target this instance group'), read_only=True)
|
||||||
instances = serializers.SerializerMethodField()
|
instances = serializers.SerializerMethodField()
|
||||||
is_container_group = serializers.BooleanField(
|
is_container_group = serializers.BooleanField(
|
||||||
@@ -5071,6 +5019,22 @@ class InstanceGroupSerializer(BaseSerializer):
|
|||||||
label=_('Policy Instance Minimum'),
|
label=_('Policy Instance Minimum'),
|
||||||
help_text=_("Static minimum number of Instances that will be automatically assign to " "this group when new instances come online."),
|
help_text=_("Static minimum number of Instances that will be automatically assign to " "this group when new instances come online."),
|
||||||
)
|
)
|
||||||
|
max_concurrent_jobs = serializers.IntegerField(
|
||||||
|
default=0,
|
||||||
|
min_value=0,
|
||||||
|
required=False,
|
||||||
|
initial=0,
|
||||||
|
label=_('Max Concurrent Jobs'),
|
||||||
|
help_text=_("Maximum number of concurrent jobs to run on a group. When set to zero, no maximum is enforced."),
|
||||||
|
)
|
||||||
|
max_forks = serializers.IntegerField(
|
||||||
|
default=0,
|
||||||
|
min_value=0,
|
||||||
|
required=False,
|
||||||
|
initial=0,
|
||||||
|
label=_('Max Forks'),
|
||||||
|
help_text=_("Maximum number of forks to execute concurrently on a group. When set to zero, no maximum is enforced."),
|
||||||
|
)
|
||||||
policy_instance_list = serializers.ListField(
|
policy_instance_list = serializers.ListField(
|
||||||
child=serializers.CharField(),
|
child=serializers.CharField(),
|
||||||
required=False,
|
required=False,
|
||||||
@@ -5092,6 +5056,8 @@ class InstanceGroupSerializer(BaseSerializer):
|
|||||||
"consumed_capacity",
|
"consumed_capacity",
|
||||||
"percent_capacity_remaining",
|
"percent_capacity_remaining",
|
||||||
"jobs_running",
|
"jobs_running",
|
||||||
|
"max_concurrent_jobs",
|
||||||
|
"max_forks",
|
||||||
"jobs_total",
|
"jobs_total",
|
||||||
"instances",
|
"instances",
|
||||||
"is_container_group",
|
"is_container_group",
|
||||||
@@ -5173,32 +5139,42 @@ class InstanceGroupSerializer(BaseSerializer):
|
|||||||
# Store capacity values (globally computed) in the context
|
# Store capacity values (globally computed) in the context
|
||||||
if 'task_manager_igs' not in self.context:
|
if 'task_manager_igs' not in self.context:
|
||||||
instance_groups_queryset = None
|
instance_groups_queryset = None
|
||||||
jobs_qs = UnifiedJob.objects.filter(status__in=('running', 'waiting'))
|
|
||||||
if self.parent: # Is ListView:
|
if self.parent: # Is ListView:
|
||||||
instance_groups_queryset = self.parent.instance
|
instance_groups_queryset = self.parent.instance
|
||||||
|
|
||||||
instances = TaskManagerInstances(jobs_qs)
|
tm_models = TaskManagerModels.init_with_consumed_capacity(
|
||||||
instance_groups = TaskManagerInstanceGroups(instances_by_hostname=instances, instance_groups_queryset=instance_groups_queryset)
|
instance_fields=['uuid', 'version', 'capacity', 'cpu', 'memory', 'managed_by_policy', 'enabled'],
|
||||||
|
instance_groups_queryset=instance_groups_queryset,
|
||||||
|
)
|
||||||
|
|
||||||
self.context['task_manager_igs'] = instance_groups
|
self.context['task_manager_igs'] = tm_models.instance_groups
|
||||||
return self.context['task_manager_igs']
|
return self.context['task_manager_igs']
|
||||||
|
|
||||||
def get_consumed_capacity(self, obj):
|
def get_consumed_capacity(self, obj):
|
||||||
ig_mgr = self.get_ig_mgr()
|
ig_mgr = self.get_ig_mgr()
|
||||||
return ig_mgr.get_consumed_capacity(obj.name)
|
return ig_mgr.get_consumed_capacity(obj.name)
|
||||||
|
|
||||||
def get_percent_capacity_remaining(self, obj):
|
def get_capacity(self, obj):
|
||||||
if not obj.capacity:
|
|
||||||
return 0.0
|
|
||||||
ig_mgr = self.get_ig_mgr()
|
ig_mgr = self.get_ig_mgr()
|
||||||
return float("{0:.2f}".format((float(ig_mgr.get_remaining_capacity(obj.name)) / (float(obj.capacity))) * 100))
|
return ig_mgr.get_capacity(obj.name)
|
||||||
|
|
||||||
|
def get_percent_capacity_remaining(self, obj):
|
||||||
|
capacity = self.get_capacity(obj)
|
||||||
|
if not capacity:
|
||||||
|
return 0.0
|
||||||
|
consumed_capacity = self.get_consumed_capacity(obj)
|
||||||
|
return float("{0:.2f}".format(((float(capacity) - float(consumed_capacity)) / (float(capacity))) * 100))
|
||||||
|
|
||||||
def get_instances(self, obj):
|
def get_instances(self, obj):
|
||||||
return obj.instances.count()
|
ig_mgr = self.get_ig_mgr()
|
||||||
|
return len(ig_mgr.get_instances(obj.name))
|
||||||
|
|
||||||
|
def get_jobs_running(self, obj):
|
||||||
|
ig_mgr = self.get_ig_mgr()
|
||||||
|
return ig_mgr.get_jobs_running(obj.name)
|
||||||
|
|
||||||
|
|
||||||
class ActivityStreamSerializer(BaseSerializer):
|
class ActivityStreamSerializer(BaseSerializer):
|
||||||
|
|
||||||
changes = serializers.SerializerMethodField()
|
changes = serializers.SerializerMethodField()
|
||||||
object_association = serializers.SerializerMethodField(help_text=_("When present, shows the field name of the role or relationship that changed."))
|
object_association = serializers.SerializerMethodField(help_text=_("When present, shows the field name of the role or relationship that changed."))
|
||||||
object_type = serializers.SerializerMethodField(help_text=_("When present, shows the model on which the role or relationship was defined."))
|
object_type = serializers.SerializerMethodField(help_text=_("When present, shows the model on which the role or relationship was defined."))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Make a GET request to this resource to retrieve aggregate statistics about inven
|
|||||||
Including fetching the number of total hosts tracked by Tower over an amount of time and the current success or
|
Including fetching the number of total hosts tracked by Tower over an amount of time and the current success or
|
||||||
failed status of hosts which have run jobs within an Inventory.
|
failed status of hosts which have run jobs within an Inventory.
|
||||||
|
|
||||||
## Parmeters and Filtering
|
## Parameters and Filtering
|
||||||
|
|
||||||
The `period` of the data can be adjusted with:
|
The `period` of the data can be adjusted with:
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ Data about the number of hosts will be returned in the following format:
|
|||||||
Each element contains an epoch timestamp represented in seconds and a numerical value indicating
|
Each element contains an epoch timestamp represented in seconds and a numerical value indicating
|
||||||
the number of hosts that exist at a given moment
|
the number of hosts that exist at a given moment
|
||||||
|
|
||||||
Data about failed and successfull hosts by inventory will be given as:
|
Data about failed and successful hosts by inventory will be given as:
|
||||||
|
|
||||||
{
|
{
|
||||||
"sources": [
|
"sources": [
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Make a GET request to this resource to retrieve aggregate statistics about job runs suitable for graphing.
|
Make a GET request to this resource to retrieve aggregate statistics about job runs suitable for graphing.
|
||||||
|
|
||||||
## Parmeters and Filtering
|
## Parameters and Filtering
|
||||||
|
|
||||||
The `period` of the data can be adjusted with:
|
The `period` of the data can be adjusted with:
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ inventory sources:
|
|||||||
* `inventory_update`: ID of the inventory update job that was started.
|
* `inventory_update`: ID of the inventory update job that was started.
|
||||||
(integer, read-only)
|
(integer, read-only)
|
||||||
* `project_update`: ID of the project update job that was started if this inventory source is an SCM source.
|
* `project_update`: ID of the project update job that was started if this inventory source is an SCM source.
|
||||||
(interger, read-only, optional)
|
(integer, read-only, optional)
|
||||||
|
|
||||||
Note: All manual inventory sources (source="") will be ignored by the update_inventory_sources endpoint. This endpoint will not update inventory sources for Smart Inventories.
|
Note: All manual inventory sources (source="") will be ignored by the update_inventory_sources endpoint. This endpoint will not update inventory sources for Smart Inventories.
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ receptor_work_commands:
|
|||||||
command: ansible-runner
|
command: ansible-runner
|
||||||
params: worker
|
params: worker
|
||||||
allowruntimeparams: true
|
allowruntimeparams: true
|
||||||
verifysignature: {{ sign_work }}
|
verifysignature: true
|
||||||
custom_worksign_public_keyfile: receptor/work-public-key.pem
|
custom_worksign_public_keyfile: receptor/work-public-key.pem
|
||||||
custom_tls_certfile: receptor/tls/receptor.crt
|
custom_tls_certfile: receptor/tls/receptor.crt
|
||||||
custom_tls_keyfile: receptor/tls/receptor.key
|
custom_tls_keyfile: receptor/tls/receptor.key
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ class HostnameRegexValidator(RegexValidator):
|
|||||||
return f"regex={self.regex}, message={self.message}, code={self.code}, inverse_match={self.inverse_match}, flags={self.flags}"
|
return f"regex={self.regex}, message={self.message}, code={self.code}, inverse_match={self.inverse_match}, flags={self.flags}"
|
||||||
|
|
||||||
def __validate(self, value):
|
def __validate(self, value):
|
||||||
|
|
||||||
if ' ' in value:
|
if ' ' in value:
|
||||||
return False, ValidationError("whitespaces in hostnames are illegal")
|
return False, ValidationError("whitespaces in hostnames are illegal")
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@ from rest_framework import status
|
|||||||
# Red Hat has an OID namespace (RHANANA). Receptor has its own designation under that.
|
# Red Hat has an OID namespace (RHANANA). Receptor has its own designation under that.
|
||||||
RECEPTOR_OID = "1.3.6.1.4.1.2312.19.1"
|
RECEPTOR_OID = "1.3.6.1.4.1.2312.19.1"
|
||||||
|
|
||||||
|
|
||||||
# generate install bundle for the instance
|
# generate install bundle for the instance
|
||||||
# install bundle directory structure
|
# install bundle directory structure
|
||||||
# ├── install_receptor.yml (playbook)
|
# ├── install_receptor.yml (playbook)
|
||||||
@@ -40,7 +41,6 @@ RECEPTOR_OID = "1.3.6.1.4.1.2312.19.1"
|
|||||||
# │ └── work-public-key.pem
|
# │ └── work-public-key.pem
|
||||||
# └── requirements.yml
|
# └── requirements.yml
|
||||||
class InstanceInstallBundle(GenericAPIView):
|
class InstanceInstallBundle(GenericAPIView):
|
||||||
|
|
||||||
name = _('Install Bundle')
|
name = _('Install Bundle')
|
||||||
model = models.Instance
|
model = models.Instance
|
||||||
serializer_class = serializers.InstanceSerializer
|
serializer_class = serializers.InstanceSerializer
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ logger = logging.getLogger('awx.api.views.organization')
|
|||||||
|
|
||||||
|
|
||||||
class InventoryUpdateEventsList(SubListAPIView):
|
class InventoryUpdateEventsList(SubListAPIView):
|
||||||
|
|
||||||
model = InventoryUpdateEvent
|
model = InventoryUpdateEvent
|
||||||
serializer_class = InventoryUpdateEventSerializer
|
serializer_class = InventoryUpdateEventSerializer
|
||||||
parent_model = InventoryUpdate
|
parent_model = InventoryUpdate
|
||||||
@@ -66,13 +65,11 @@ class InventoryUpdateEventsList(SubListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryList(ListCreateAPIView):
|
class InventoryList(ListCreateAPIView):
|
||||||
|
|
||||||
model = Inventory
|
model = Inventory
|
||||||
serializer_class = InventorySerializer
|
serializer_class = InventorySerializer
|
||||||
|
|
||||||
|
|
||||||
class InventoryDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
class InventoryDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
||||||
|
|
||||||
model = Inventory
|
model = Inventory
|
||||||
serializer_class = InventorySerializer
|
serializer_class = InventorySerializer
|
||||||
|
|
||||||
@@ -98,7 +95,6 @@ class InventoryDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIVie
|
|||||||
|
|
||||||
|
|
||||||
class InventoryActivityStreamList(SubListAPIView):
|
class InventoryActivityStreamList(SubListAPIView):
|
||||||
|
|
||||||
model = ActivityStream
|
model = ActivityStream
|
||||||
serializer_class = ActivityStreamSerializer
|
serializer_class = ActivityStreamSerializer
|
||||||
parent_model = Inventory
|
parent_model = Inventory
|
||||||
@@ -113,7 +109,6 @@ class InventoryActivityStreamList(SubListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryInstanceGroupsList(SubListAttachDetachAPIView):
|
class InventoryInstanceGroupsList(SubListAttachDetachAPIView):
|
||||||
|
|
||||||
model = InstanceGroup
|
model = InstanceGroup
|
||||||
serializer_class = InstanceGroupSerializer
|
serializer_class = InstanceGroupSerializer
|
||||||
parent_model = Inventory
|
parent_model = Inventory
|
||||||
@@ -121,13 +116,11 @@ class InventoryInstanceGroupsList(SubListAttachDetachAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryAccessList(ResourceAccessList):
|
class InventoryAccessList(ResourceAccessList):
|
||||||
|
|
||||||
model = User # needs to be User for AccessLists's
|
model = User # needs to be User for AccessLists's
|
||||||
parent_model = Inventory
|
parent_model = Inventory
|
||||||
|
|
||||||
|
|
||||||
class InventoryObjectRolesList(SubListAPIView):
|
class InventoryObjectRolesList(SubListAPIView):
|
||||||
|
|
||||||
model = Role
|
model = Role
|
||||||
serializer_class = RoleSerializer
|
serializer_class = RoleSerializer
|
||||||
parent_model = Inventory
|
parent_model = Inventory
|
||||||
@@ -140,7 +133,6 @@ class InventoryObjectRolesList(SubListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryJobTemplateList(SubListAPIView):
|
class InventoryJobTemplateList(SubListAPIView):
|
||||||
|
|
||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
serializer_class = JobTemplateSerializer
|
serializer_class = JobTemplateSerializer
|
||||||
parent_model = Inventory
|
parent_model = Inventory
|
||||||
@@ -154,11 +146,9 @@ class InventoryJobTemplateList(SubListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryLabelList(LabelSubListCreateAttachDetachView):
|
class InventoryLabelList(LabelSubListCreateAttachDetachView):
|
||||||
|
|
||||||
parent_model = Inventory
|
parent_model = Inventory
|
||||||
|
|
||||||
|
|
||||||
class InventoryCopy(CopyAPIView):
|
class InventoryCopy(CopyAPIView):
|
||||||
|
|
||||||
model = Inventory
|
model = Inventory
|
||||||
copy_return_serializer_class = InventorySerializer
|
copy_return_serializer_class = InventorySerializer
|
||||||
|
|||||||
@@ -59,13 +59,11 @@ class LabelSubListCreateAttachDetachView(SubListCreateAttachDetachAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class LabelDetail(RetrieveUpdateAPIView):
|
class LabelDetail(RetrieveUpdateAPIView):
|
||||||
|
|
||||||
model = Label
|
model = Label
|
||||||
serializer_class = LabelSerializer
|
serializer_class = LabelSerializer
|
||||||
|
|
||||||
|
|
||||||
class LabelList(ListCreateAPIView):
|
class LabelList(ListCreateAPIView):
|
||||||
|
|
||||||
name = _("Labels")
|
name = _("Labels")
|
||||||
model = Label
|
model = Label
|
||||||
serializer_class = LabelSerializer
|
serializer_class = LabelSerializer
|
||||||
|
|||||||
@@ -10,13 +10,11 @@ from awx.main.models import InstanceLink, Instance
|
|||||||
|
|
||||||
|
|
||||||
class MeshVisualizer(APIView):
|
class MeshVisualizer(APIView):
|
||||||
|
|
||||||
name = _("Mesh Visualizer")
|
name = _("Mesh Visualizer")
|
||||||
permission_classes = (IsSystemAdminOrAuditor,)
|
permission_classes = (IsSystemAdminOrAuditor,)
|
||||||
swagger_topic = "System Configuration"
|
swagger_topic = "System Configuration"
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'nodes': InstanceNodeSerializer(Instance.objects.all(), many=True).data,
|
'nodes': InstanceNodeSerializer(Instance.objects.all(), many=True).data,
|
||||||
'links': InstanceLinkSerializer(InstanceLink.objects.select_related('target', 'source'), many=True).data,
|
'links': InstanceLinkSerializer(InstanceLink.objects.select_related('target', 'source'), many=True).data,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -25,15 +27,19 @@ logger = logging.getLogger('awx.analytics')
|
|||||||
|
|
||||||
|
|
||||||
class MetricsView(APIView):
|
class MetricsView(APIView):
|
||||||
|
|
||||||
name = _('Metrics')
|
name = _('Metrics')
|
||||||
swagger_topic = 'Metrics'
|
swagger_topic = 'Metrics'
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ logger = logging.getLogger('awx.api.views.organization')
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationList(OrganizationCountsMixin, ListCreateAPIView):
|
class OrganizationList(OrganizationCountsMixin, ListCreateAPIView):
|
||||||
|
|
||||||
model = Organization
|
model = Organization
|
||||||
serializer_class = OrganizationSerializer
|
serializer_class = OrganizationSerializer
|
||||||
|
|
||||||
@@ -70,7 +69,6 @@ class OrganizationList(OrganizationCountsMixin, ListCreateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
||||||
|
|
||||||
model = Organization
|
model = Organization
|
||||||
serializer_class = OrganizationSerializer
|
serializer_class = OrganizationSerializer
|
||||||
|
|
||||||
@@ -106,7 +104,6 @@ class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPI
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationInventoriesList(SubListAPIView):
|
class OrganizationInventoriesList(SubListAPIView):
|
||||||
|
|
||||||
model = Inventory
|
model = Inventory
|
||||||
serializer_class = InventorySerializer
|
serializer_class = InventorySerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
@@ -114,7 +111,6 @@ class OrganizationInventoriesList(SubListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationUsersList(BaseUsersList):
|
class OrganizationUsersList(BaseUsersList):
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
@@ -123,7 +119,6 @@ class OrganizationUsersList(BaseUsersList):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationAdminsList(BaseUsersList):
|
class OrganizationAdminsList(BaseUsersList):
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
@@ -132,7 +127,6 @@ class OrganizationAdminsList(BaseUsersList):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationProjectsList(SubListCreateAPIView):
|
class OrganizationProjectsList(SubListCreateAPIView):
|
||||||
|
|
||||||
model = Project
|
model = Project
|
||||||
serializer_class = ProjectSerializer
|
serializer_class = ProjectSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
@@ -140,7 +134,6 @@ class OrganizationProjectsList(SubListCreateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationExecutionEnvironmentsList(SubListCreateAttachDetachAPIView):
|
class OrganizationExecutionEnvironmentsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = ExecutionEnvironment
|
model = ExecutionEnvironment
|
||||||
serializer_class = ExecutionEnvironmentSerializer
|
serializer_class = ExecutionEnvironmentSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
@@ -150,7 +143,6 @@ class OrganizationExecutionEnvironmentsList(SubListCreateAttachDetachAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationJobTemplatesList(SubListCreateAPIView):
|
class OrganizationJobTemplatesList(SubListCreateAPIView):
|
||||||
|
|
||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
serializer_class = JobTemplateSerializer
|
serializer_class = JobTemplateSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
@@ -158,7 +150,6 @@ class OrganizationJobTemplatesList(SubListCreateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationWorkflowJobTemplatesList(SubListCreateAPIView):
|
class OrganizationWorkflowJobTemplatesList(SubListCreateAPIView):
|
||||||
|
|
||||||
model = WorkflowJobTemplate
|
model = WorkflowJobTemplate
|
||||||
serializer_class = WorkflowJobTemplateSerializer
|
serializer_class = WorkflowJobTemplateSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
@@ -166,7 +157,6 @@ class OrganizationWorkflowJobTemplatesList(SubListCreateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationTeamsList(SubListCreateAttachDetachAPIView):
|
class OrganizationTeamsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Team
|
model = Team
|
||||||
serializer_class = TeamSerializer
|
serializer_class = TeamSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
@@ -175,7 +165,6 @@ class OrganizationTeamsList(SubListCreateAttachDetachAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationActivityStreamList(SubListAPIView):
|
class OrganizationActivityStreamList(SubListAPIView):
|
||||||
|
|
||||||
model = ActivityStream
|
model = ActivityStream
|
||||||
serializer_class = ActivityStreamSerializer
|
serializer_class = ActivityStreamSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
@@ -184,7 +173,6 @@ class OrganizationActivityStreamList(SubListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationNotificationTemplatesList(SubListCreateAttachDetachAPIView):
|
class OrganizationNotificationTemplatesList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = NotificationTemplate
|
model = NotificationTemplate
|
||||||
serializer_class = NotificationTemplateSerializer
|
serializer_class = NotificationTemplateSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
@@ -193,34 +181,28 @@ class OrganizationNotificationTemplatesList(SubListCreateAttachDetachAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
|
class OrganizationNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = NotificationTemplate
|
model = NotificationTemplate
|
||||||
serializer_class = NotificationTemplateSerializer
|
serializer_class = NotificationTemplateSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
|
|
||||||
|
|
||||||
class OrganizationNotificationTemplatesStartedList(OrganizationNotificationTemplatesAnyList):
|
class OrganizationNotificationTemplatesStartedList(OrganizationNotificationTemplatesAnyList):
|
||||||
|
|
||||||
relationship = 'notification_templates_started'
|
relationship = 'notification_templates_started'
|
||||||
|
|
||||||
|
|
||||||
class OrganizationNotificationTemplatesErrorList(OrganizationNotificationTemplatesAnyList):
|
class OrganizationNotificationTemplatesErrorList(OrganizationNotificationTemplatesAnyList):
|
||||||
|
|
||||||
relationship = 'notification_templates_error'
|
relationship = 'notification_templates_error'
|
||||||
|
|
||||||
|
|
||||||
class OrganizationNotificationTemplatesSuccessList(OrganizationNotificationTemplatesAnyList):
|
class OrganizationNotificationTemplatesSuccessList(OrganizationNotificationTemplatesAnyList):
|
||||||
|
|
||||||
relationship = 'notification_templates_success'
|
relationship = 'notification_templates_success'
|
||||||
|
|
||||||
|
|
||||||
class OrganizationNotificationTemplatesApprovalList(OrganizationNotificationTemplatesAnyList):
|
class OrganizationNotificationTemplatesApprovalList(OrganizationNotificationTemplatesAnyList):
|
||||||
|
|
||||||
relationship = 'notification_templates_approvals'
|
relationship = 'notification_templates_approvals'
|
||||||
|
|
||||||
|
|
||||||
class OrganizationInstanceGroupsList(SubListAttachDetachAPIView):
|
class OrganizationInstanceGroupsList(SubListAttachDetachAPIView):
|
||||||
|
|
||||||
model = InstanceGroup
|
model = InstanceGroup
|
||||||
serializer_class = InstanceGroupSerializer
|
serializer_class = InstanceGroupSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
@@ -228,7 +210,6 @@ class OrganizationInstanceGroupsList(SubListAttachDetachAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationGalaxyCredentialsList(SubListAttachDetachAPIView):
|
class OrganizationGalaxyCredentialsList(SubListAttachDetachAPIView):
|
||||||
|
|
||||||
model = Credential
|
model = Credential
|
||||||
serializer_class = CredentialSerializer
|
serializer_class = CredentialSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
@@ -240,13 +221,11 @@ class OrganizationGalaxyCredentialsList(SubListAttachDetachAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizationAccessList(ResourceAccessList):
|
class OrganizationAccessList(ResourceAccessList):
|
||||||
|
|
||||||
model = User # needs to be User for AccessLists's
|
model = User # needs to be User for AccessLists's
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
|
|
||||||
|
|
||||||
class OrganizationObjectRolesList(SubListAPIView):
|
class OrganizationObjectRolesList(SubListAPIView):
|
||||||
|
|
||||||
model = Role
|
model = Role
|
||||||
serializer_class = RoleSerializer
|
serializer_class = RoleSerializer
|
||||||
parent_model = Organization
|
parent_model = Organization
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ logger = logging.getLogger('awx.api.views.root')
|
|||||||
|
|
||||||
|
|
||||||
class ApiRootView(APIView):
|
class ApiRootView(APIView):
|
||||||
|
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
name = _('REST API')
|
name = _('REST API')
|
||||||
versioning_class = None
|
versioning_class = None
|
||||||
@@ -59,7 +58,6 @@ class ApiRootView(APIView):
|
|||||||
|
|
||||||
|
|
||||||
class ApiOAuthAuthorizationRootView(APIView):
|
class ApiOAuthAuthorizationRootView(APIView):
|
||||||
|
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
name = _("API OAuth 2 Authorization Root")
|
name = _("API OAuth 2 Authorization Root")
|
||||||
versioning_class = None
|
versioning_class = None
|
||||||
@@ -74,7 +72,6 @@ class ApiOAuthAuthorizationRootView(APIView):
|
|||||||
|
|
||||||
|
|
||||||
class ApiVersionRootView(APIView):
|
class ApiVersionRootView(APIView):
|
||||||
|
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
swagger_topic = 'Versioning'
|
swagger_topic = 'Versioning'
|
||||||
|
|
||||||
@@ -172,7 +169,6 @@ class ApiV2PingView(APIView):
|
|||||||
|
|
||||||
|
|
||||||
class ApiV2SubscriptionView(APIView):
|
class ApiV2SubscriptionView(APIView):
|
||||||
|
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
name = _('Subscriptions')
|
name = _('Subscriptions')
|
||||||
swagger_topic = 'System Configuration'
|
swagger_topic = 'System Configuration'
|
||||||
@@ -212,7 +208,6 @@ class ApiV2SubscriptionView(APIView):
|
|||||||
|
|
||||||
|
|
||||||
class ApiV2AttachView(APIView):
|
class ApiV2AttachView(APIView):
|
||||||
|
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
name = _('Attach Subscription')
|
name = _('Attach Subscription')
|
||||||
swagger_topic = 'System Configuration'
|
swagger_topic = 'System Configuration'
|
||||||
@@ -230,7 +225,6 @@ class ApiV2AttachView(APIView):
|
|||||||
user = getattr(settings, 'SUBSCRIPTIONS_USERNAME', None)
|
user = getattr(settings, 'SUBSCRIPTIONS_USERNAME', None)
|
||||||
pw = getattr(settings, 'SUBSCRIPTIONS_PASSWORD', None)
|
pw = getattr(settings, 'SUBSCRIPTIONS_PASSWORD', None)
|
||||||
if pool_id and user and pw:
|
if pool_id and user and pw:
|
||||||
|
|
||||||
data = request.data.copy()
|
data = request.data.copy()
|
||||||
try:
|
try:
|
||||||
with set_environ(**settings.AWX_TASK_ENV):
|
with set_environ(**settings.AWX_TASK_ENV):
|
||||||
@@ -258,7 +252,6 @@ class ApiV2AttachView(APIView):
|
|||||||
|
|
||||||
|
|
||||||
class ApiV2ConfigView(APIView):
|
class ApiV2ConfigView(APIView):
|
||||||
|
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
name = _('Configuration')
|
name = _('Configuration')
|
||||||
swagger_topic = 'System Configuration'
|
swagger_topic = 'System Configuration'
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
|
|
||||||
class ConfConfig(AppConfig):
|
class ConfConfig(AppConfig):
|
||||||
|
|
||||||
name = 'awx.conf'
|
name = 'awx.conf'
|
||||||
verbose_name = _('Configuration')
|
verbose_name = _('Configuration')
|
||||||
|
|
||||||
@@ -16,7 +15,6 @@ class ConfConfig(AppConfig):
|
|||||||
self.module.autodiscover()
|
self.module.autodiscover()
|
||||||
|
|
||||||
if not set(sys.argv) & {'migrate', 'check_migrations'}:
|
if not set(sys.argv) & {'migrate', 'check_migrations'}:
|
||||||
|
|
||||||
from .settings import SettingsWrapper
|
from .settings import SettingsWrapper
|
||||||
|
|
||||||
SettingsWrapper.initialize()
|
SettingsWrapper.initialize()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ logger = logging.getLogger('awx.conf.fields')
|
|||||||
# Use DRF fields to convert/validate settings:
|
# Use DRF fields to convert/validate settings:
|
||||||
# - to_representation(obj) should convert a native Python object to a primitive
|
# - to_representation(obj) should convert a native Python object to a primitive
|
||||||
# serializable type. This primitive type will be what is presented in the API
|
# serializable type. This primitive type will be what is presented in the API
|
||||||
# and stored in the JSON field in the datbase.
|
# and stored in the JSON field in the database.
|
||||||
# - to_internal_value(data) should convert the primitive type back into the
|
# - to_internal_value(data) should convert the primitive type back into the
|
||||||
# appropriate Python type to be used in settings.
|
# appropriate Python type to be used in settings.
|
||||||
|
|
||||||
@@ -47,7 +47,6 @@ class IntegerField(IntegerField):
|
|||||||
|
|
||||||
|
|
||||||
class StringListField(ListField):
|
class StringListField(ListField):
|
||||||
|
|
||||||
child = CharField()
|
child = CharField()
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
@@ -57,7 +56,6 @@ class StringListField(ListField):
|
|||||||
|
|
||||||
|
|
||||||
class StringListBooleanField(ListField):
|
class StringListBooleanField(ListField):
|
||||||
|
|
||||||
default_error_messages = {'type_error': _('Expected None, True, False, a string or list of strings but got {input_type} instead.')}
|
default_error_messages = {'type_error': _('Expected None, True, False, a string or list of strings but got {input_type} instead.')}
|
||||||
child = CharField()
|
child = CharField()
|
||||||
|
|
||||||
@@ -96,7 +94,6 @@ class StringListBooleanField(ListField):
|
|||||||
|
|
||||||
|
|
||||||
class StringListPathField(StringListField):
|
class StringListPathField(StringListField):
|
||||||
|
|
||||||
default_error_messages = {'type_error': _('Expected list of strings but got {input_type} instead.'), 'path_error': _('{path} is not a valid path choice.')}
|
default_error_messages = {'type_error': _('Expected list of strings but got {input_type} instead.'), 'path_error': _('{path} is not a valid path choice.')}
|
||||||
|
|
||||||
def to_internal_value(self, paths):
|
def to_internal_value(self, paths):
|
||||||
@@ -126,7 +123,6 @@ class StringListIsolatedPathField(StringListField):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def to_internal_value(self, paths):
|
def to_internal_value(self, paths):
|
||||||
|
|
||||||
if isinstance(paths, (list, tuple)):
|
if isinstance(paths, (list, tuple)):
|
||||||
for p in paths:
|
for p in paths:
|
||||||
if not isinstance(p, str):
|
if not isinstance(p, str):
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import awx.main.fields
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
|
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ def revert_tower_settings(apps, schema_editor):
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [('conf', '0001_initial'), ('main', '0004_squashed_v310_release')]
|
dependencies = [('conf', '0001_initial'), ('main', '0004_squashed_v310_release')]
|
||||||
|
|
||||||
run_before = [('main', '0005_squashed_v310_v313_updates')]
|
run_before = [('main', '0005_squashed_v310_v313_updates')]
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import awx.main.fields
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [('conf', '0002_v310_copy_tower_settings')]
|
dependencies = [('conf', '0002_v310_copy_tower_settings')]
|
||||||
|
|
||||||
operations = [migrations.AlterField(model_name='setting', name='value', field=awx.main.fields.JSONBlob(null=True))]
|
operations = [migrations.AlterField(model_name='setting', name='value', field=awx.main.fields.JSONBlob(null=True))]
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from django.db import migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [('conf', '0003_v310_JSONField_changes')]
|
dependencies = [('conf', '0003_v310_JSONField_changes')]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ def reverse_copy_session_settings(apps, schema_editor):
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [('conf', '0004_v320_reencrypt')]
|
dependencies = [('conf', '0004_v320_reencrypt')]
|
||||||
|
|
||||||
operations = [migrations.RunPython(copy_session_settings, reverse_copy_session_settings)]
|
operations = [migrations.RunPython(copy_session_settings, reverse_copy_session_settings)]
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from django.db import migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [('conf', '0005_v330_rename_two_session_settings')]
|
dependencies = [('conf', '0005_v330_rename_two_session_settings')]
|
||||||
|
|
||||||
operations = [migrations.RunPython(fill_ldap_group_type_params)]
|
operations = [migrations.RunPython(fill_ldap_group_type_params)]
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ def copy_allowed_ips(apps, schema_editor):
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [('conf', '0006_v331_ldap_group_type')]
|
dependencies = [('conf', '0006_v331_ldap_group_type')]
|
||||||
|
|
||||||
operations = [migrations.RunPython(copy_allowed_ips)]
|
operations = [migrations.RunPython(copy_allowed_ips)]
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ def _noop(apps, schema_editor):
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [('conf', '0007_v380_rename_more_settings')]
|
dependencies = [('conf', '0007_v380_rename_more_settings')]
|
||||||
|
|
||||||
operations = [migrations.RunPython(clear_old_license, _noop), migrations.RunPython(prefill_rh_credentials, _noop)]
|
operations = [migrations.RunPython(clear_old_license, _noop), migrations.RunPython(prefill_rh_credentials, _noop)]
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ def rename_proot_settings(apps, schema_editor):
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [('conf', '0008_subscriptions')]
|
dependencies = [('conf', '0008_subscriptions')]
|
||||||
|
|
||||||
operations = [migrations.RunPython(rename_proot_settings)]
|
operations = [migrations.RunPython(rename_proot_settings)]
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.timezone import now
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('awx.conf.migrations')
|
||||||
|
|
||||||
|
|
||||||
def fill_ldap_group_type_params(apps, schema_editor):
|
def fill_ldap_group_type_params(apps, schema_editor):
|
||||||
@@ -15,7 +19,7 @@ def fill_ldap_group_type_params(apps, schema_editor):
|
|||||||
entry = qs[0]
|
entry = qs[0]
|
||||||
group_type_params = entry.value
|
group_type_params = entry.value
|
||||||
else:
|
else:
|
||||||
entry = Setting(key='AUTH_LDAP_GROUP_TYPE_PARAMS', value=group_type_params, created=now(), modified=now())
|
return # for new installs we prefer to use the default value
|
||||||
|
|
||||||
init_attrs = set(inspect.getfullargspec(group_type.__init__).args[1:])
|
init_attrs = set(inspect.getfullargspec(group_type.__init__).args[1:])
|
||||||
for k in list(group_type_params.keys()):
|
for k in list(group_type_params.keys()):
|
||||||
@@ -23,4 +27,5 @@ def fill_ldap_group_type_params(apps, schema_editor):
|
|||||||
del group_type_params[k]
|
del group_type_params[k]
|
||||||
|
|
||||||
entry.value = group_type_params
|
entry.value = group_type_params
|
||||||
|
logger.warning(f'Migration updating AUTH_LDAP_GROUP_TYPE_PARAMS with value {entry.value}')
|
||||||
entry.save()
|
entry.save()
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ __all__ = ['rename_setting']
|
|||||||
|
|
||||||
|
|
||||||
def rename_setting(apps, schema_editor, old_key, new_key):
|
def rename_setting(apps, schema_editor, old_key, new_key):
|
||||||
|
|
||||||
old_setting = None
|
old_setting = None
|
||||||
Setting = apps.get_model('conf', 'Setting')
|
Setting = apps.get_model('conf', 'Setting')
|
||||||
if Setting.objects.filter(key=new_key).exists() or hasattr(settings, new_key):
|
if Setting.objects.filter(key=new_key).exists() or hasattr(settings, new_key):
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ __all__ = ['Setting']
|
|||||||
|
|
||||||
|
|
||||||
class Setting(CreatedModifiedModel):
|
class Setting(CreatedModifiedModel):
|
||||||
|
|
||||||
key = models.CharField(max_length=255)
|
key = models.CharField(max_length=255)
|
||||||
value = JSONBlob(null=True)
|
value = JSONBlob(null=True)
|
||||||
user = prevent_search(models.ForeignKey('auth.User', related_name='settings', default=None, null=True, editable=False, on_delete=models.CASCADE))
|
user = prevent_search(models.ForeignKey('auth.User', related_name='settings', default=None, null=True, editable=False, on_delete=models.CASCADE))
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ def filter_sensitive(registry, key, value):
|
|||||||
|
|
||||||
|
|
||||||
class TransientSetting(object):
|
class TransientSetting(object):
|
||||||
|
|
||||||
__slots__ = ('pk', 'value')
|
__slots__ = ('pk', 'value')
|
||||||
|
|
||||||
def __init__(self, pk, value):
|
def __init__(self, pk, value):
|
||||||
|
|||||||
25
awx/conf/tests/functional/test_migrations.py
Normal file
25
awx/conf/tests/functional/test_migrations.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from awx.conf.migrations._ldap_group_type import fill_ldap_group_type_params
|
||||||
|
from awx.conf.models import Setting
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_fill_group_type_params_no_op():
|
||||||
|
fill_ldap_group_type_params(apps, 'dont-use-me')
|
||||||
|
assert Setting.objects.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_keep_old_setting_with_default_value():
|
||||||
|
Setting.objects.create(key='AUTH_LDAP_GROUP_TYPE', value={'name_attr': 'cn', 'member_attr': 'member'})
|
||||||
|
fill_ldap_group_type_params(apps, 'dont-use-me')
|
||||||
|
assert Setting.objects.count() == 1
|
||||||
|
s = Setting.objects.first()
|
||||||
|
assert s.value == {'name_attr': 'cn', 'member_attr': 'member'}
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: would be good to test the removal of attributes by migration
|
||||||
|
# but this requires fighting with the validator and is not done here
|
||||||
@@ -5,7 +5,6 @@ from awx.conf.fields import StringListBooleanField, StringListPathField, ListTup
|
|||||||
|
|
||||||
|
|
||||||
class TestStringListBooleanField:
|
class TestStringListBooleanField:
|
||||||
|
|
||||||
FIELD_VALUES = [
|
FIELD_VALUES = [
|
||||||
("hello", "hello"),
|
("hello", "hello"),
|
||||||
(("a", "b"), ["a", "b"]),
|
(("a", "b"), ["a", "b"]),
|
||||||
@@ -53,7 +52,6 @@ class TestStringListBooleanField:
|
|||||||
|
|
||||||
|
|
||||||
class TestListTuplesField:
|
class TestListTuplesField:
|
||||||
|
|
||||||
FIELD_VALUES = [([('a', 'b'), ('abc', '123')], [("a", "b"), ("abc", "123")])]
|
FIELD_VALUES = [([('a', 'b'), ('abc', '123')], [("a", "b"), ("abc", "123")])]
|
||||||
|
|
||||||
FIELD_VALUES_INVALID = [("abc", type("abc")), ([('a', 'b', 'c'), ('abc', '123', '456')], type(('a',))), (['a', 'b'], type('a')), (123, type(123))]
|
FIELD_VALUES_INVALID = [("abc", type("abc")), ([('a', 'b', 'c'), ('abc', '123', '456')], type(('a',))), (['a', 'b'], type('a')), (123, type(123))]
|
||||||
@@ -73,7 +71,6 @@ class TestListTuplesField:
|
|||||||
|
|
||||||
|
|
||||||
class TestStringListPathField:
|
class TestStringListPathField:
|
||||||
|
|
||||||
FIELD_VALUES = [
|
FIELD_VALUES = [
|
||||||
((".", "..", "/"), [".", "..", "/"]),
|
((".", "..", "/"), [".", "..", "/"]),
|
||||||
(("/home",), ["/home"]),
|
(("/home",), ["/home"]),
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ SettingCategory = collections.namedtuple('SettingCategory', ('url', 'slug', 'nam
|
|||||||
|
|
||||||
|
|
||||||
class SettingCategoryList(ListAPIView):
|
class SettingCategoryList(ListAPIView):
|
||||||
|
|
||||||
model = Setting # Not exactly, but needed for the view.
|
model = Setting # Not exactly, but needed for the view.
|
||||||
serializer_class = SettingCategorySerializer
|
serializer_class = SettingCategorySerializer
|
||||||
filter_backends = []
|
filter_backends = []
|
||||||
@@ -58,7 +57,6 @@ class SettingCategoryList(ListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
|
class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
|
||||||
|
|
||||||
model = Setting # Not exactly, but needed for the view.
|
model = Setting # Not exactly, but needed for the view.
|
||||||
serializer_class = SettingSingletonSerializer
|
serializer_class = SettingSingletonSerializer
|
||||||
filter_backends = []
|
filter_backends = []
|
||||||
@@ -146,7 +144,6 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class SettingLoggingTest(GenericAPIView):
|
class SettingLoggingTest(GenericAPIView):
|
||||||
|
|
||||||
name = _('Logging Connectivity Test')
|
name = _('Logging Connectivity Test')
|
||||||
model = Setting
|
model = Setting
|
||||||
serializer_class = SettingSingletonSerializer
|
serializer_class = SettingSingletonSerializer
|
||||||
@@ -183,7 +180,7 @@ class SettingLoggingTest(GenericAPIView):
|
|||||||
if not port:
|
if not port:
|
||||||
return Response({'error': 'Port required for ' + protocol}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'error': 'Port required for ' + protocol}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
else:
|
else:
|
||||||
# if http/https by this point, domain is reacheable
|
# if http/https by this point, domain is reachable
|
||||||
return Response(status=status.HTTP_202_ACCEPTED)
|
return Response(status=status.HTTP_202_ACCEPTED)
|
||||||
|
|
||||||
if protocol == 'udp':
|
if protocol == 'udp':
|
||||||
|
|||||||
@@ -1972,7 +1972,7 @@ msgid ""
|
|||||||
"HTTP headers and meta keys to search to determine remote host name or IP. "
|
"HTTP headers and meta keys to search to determine remote host name or IP. "
|
||||||
"Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if "
|
"Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if "
|
||||||
"behind a reverse proxy. See the \"Proxy Support\" section of the "
|
"behind a reverse proxy. See the \"Proxy Support\" section of the "
|
||||||
"Adminstrator guide for more details."
|
"Administrator guide for more details."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/conf.py:85
|
#: awx/main/conf.py:85
|
||||||
@@ -2457,7 +2457,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/conf.py:631
|
#: awx/main/conf.py:631
|
||||||
msgid "Maximum disk persistance for external log aggregation (in GB)"
|
msgid "Maximum disk persistence for external log aggregation (in GB)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/conf.py:633
|
#: awx/main/conf.py:633
|
||||||
@@ -2548,7 +2548,7 @@ msgid "Enable"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/constants.py:27
|
#: awx/main/constants.py:27
|
||||||
msgid "Doas"
|
msgid "Does"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: awx/main/constants.py:28
|
#: awx/main/constants.py:28
|
||||||
@@ -4801,7 +4801,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: awx/main/models/workflow.py:251
|
#: awx/main/models/workflow.py:251
|
||||||
msgid ""
|
msgid ""
|
||||||
"An identifier coresponding to the workflow job template node that this node "
|
"An identifier corresponding to the workflow job template node that this node "
|
||||||
"was created from."
|
"was created from."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -5521,7 +5521,7 @@ msgstr ""
|
|||||||
#: awx/sso/conf.py:606
|
#: awx/sso/conf.py:606
|
||||||
msgid ""
|
msgid ""
|
||||||
"Extra arguments for Google OAuth2 login. You can restrict it to only allow a "
|
"Extra arguments for Google OAuth2 login. You can restrict it to only allow a "
|
||||||
"single domain to authenticate, even if the user is logged in with multple "
|
"single domain to authenticate, even if the user is logged in with multiple "
|
||||||
"Google accounts. Refer to the documentation for more detail."
|
"Google accounts. Refer to the documentation for more detail."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -5905,7 +5905,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: awx/sso/conf.py:1290
|
#: awx/sso/conf.py:1290
|
||||||
msgid ""
|
msgid ""
|
||||||
"Create a keypair to use as a service provider (SP) and include the "
|
"Create a key pair to use as a service provider (SP) and include the "
|
||||||
"certificate content here."
|
"certificate content here."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -5915,7 +5915,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: awx/sso/conf.py:1302
|
#: awx/sso/conf.py:1302
|
||||||
msgid ""
|
msgid ""
|
||||||
"Create a keypair to use as a service provider (SP) and include the private "
|
"Create a key pair to use as a service provider (SP) and include the private "
|
||||||
"key content here."
|
"key content here."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -1971,7 +1971,7 @@ msgid ""
|
|||||||
"HTTP headers and meta keys to search to determine remote host name or IP. "
|
"HTTP headers and meta keys to search to determine remote host name or IP. "
|
||||||
"Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if "
|
"Add additional items to this list, such as \"HTTP_X_FORWARDED_FOR\", if "
|
||||||
"behind a reverse proxy. See the \"Proxy Support\" section of the "
|
"behind a reverse proxy. See the \"Proxy Support\" section of the "
|
||||||
"Adminstrator guide for more details."
|
"Administrator guide for more details."
|
||||||
msgstr "Los encabezados HTTP y las llaves de activación para buscar y determinar el nombre de host remoto o IP. Añada elementos adicionales a esta lista, como \"HTTP_X_FORWARDED_FOR\", si está detrás de un proxy inverso. Consulte la sección \"Soporte de proxy\" de la guía del adminstrador para obtener más información."
|
msgstr "Los encabezados HTTP y las llaves de activación para buscar y determinar el nombre de host remoto o IP. Añada elementos adicionales a esta lista, como \"HTTP_X_FORWARDED_FOR\", si está detrás de un proxy inverso. Consulte la sección \"Soporte de proxy\" de la guía del adminstrador para obtener más información."
|
||||||
|
|
||||||
#: awx/main/conf.py:85
|
#: awx/main/conf.py:85
|
||||||
@@ -4804,7 +4804,7 @@ msgstr "Indica que un trabajo no se creará cuando es sea True. La semántica de
|
|||||||
|
|
||||||
#: awx/main/models/workflow.py:251
|
#: awx/main/models/workflow.py:251
|
||||||
msgid ""
|
msgid ""
|
||||||
"An identifier coresponding to the workflow job template node that this node "
|
"An identifier corresponding to the workflow job template node that this node "
|
||||||
"was created from."
|
"was created from."
|
||||||
msgstr "Un identificador que corresponde al nodo de plantilla de tarea del flujo de trabajo a partir del cual se creó este nodo."
|
msgstr "Un identificador que corresponde al nodo de plantilla de tarea del flujo de trabajo a partir del cual se creó este nodo."
|
||||||
|
|
||||||
@@ -5526,7 +5526,7 @@ msgstr "Argumentos adicionales para Google OAuth2"
|
|||||||
#: awx/sso/conf.py:606
|
#: awx/sso/conf.py:606
|
||||||
msgid ""
|
msgid ""
|
||||||
"Extra arguments for Google OAuth2 login. You can restrict it to only allow a "
|
"Extra arguments for Google OAuth2 login. You can restrict it to only allow a "
|
||||||
"single domain to authenticate, even if the user is logged in with multple "
|
"single domain to authenticate, even if the user is logged in with multiple "
|
||||||
"Google accounts. Refer to the documentation for more detail."
|
"Google accounts. Refer to the documentation for more detail."
|
||||||
msgstr "Argumentos adicionales para el inicio de sesión en Google OAuth2. Puede limitarlo para permitir la autenticación de un solo dominio, incluso si el usuario ha iniciado sesión con varias cuentas de Google. Consulte la documentación para obtener información detallada."
|
msgstr "Argumentos adicionales para el inicio de sesión en Google OAuth2. Puede limitarlo para permitir la autenticación de un solo dominio, incluso si el usuario ha iniciado sesión con varias cuentas de Google. Consulte la documentación para obtener información detallada."
|
||||||
|
|
||||||
@@ -5910,7 +5910,7 @@ msgstr "Certificado público del proveedor de servicio SAML"
|
|||||||
|
|
||||||
#: awx/sso/conf.py:1290
|
#: awx/sso/conf.py:1290
|
||||||
msgid ""
|
msgid ""
|
||||||
"Create a keypair to use as a service provider (SP) and include the "
|
"Create a key pair to use as a service provider (SP) and include the "
|
||||||
"certificate content here."
|
"certificate content here."
|
||||||
msgstr "Crear un par de claves para usar como proveedor de servicio (SP) e incluir el contenido del certificado aquí."
|
msgstr "Crear un par de claves para usar como proveedor de servicio (SP) e incluir el contenido del certificado aquí."
|
||||||
|
|
||||||
@@ -5920,7 +5920,7 @@ msgstr "Clave privada del proveedor de servicio SAML"
|
|||||||
|
|
||||||
#: awx/sso/conf.py:1302
|
#: awx/sso/conf.py:1302
|
||||||
msgid ""
|
msgid ""
|
||||||
"Create a keypair to use as a service provider (SP) and include the private "
|
"Create a key pair to use as a service provider (SP) and include the private "
|
||||||
"key content here."
|
"key content here."
|
||||||
msgstr "Crear un par de claves para usar como proveedor de servicio (SP) e incluir el contenido de la clave privada aquí."
|
msgstr "Crear un par de claves para usar como proveedor de servicio (SP) e incluir el contenido de la clave privada aquí."
|
||||||
|
|
||||||
@@ -6237,4 +6237,5 @@ msgstr "%s se está actualizando."
|
|||||||
|
|
||||||
#: awx/ui/urls.py:24
|
#: awx/ui/urls.py:24
|
||||||
msgid "This page will refresh when complete."
|
msgid "This page will refresh when complete."
|
||||||
msgstr "Esta página se actualizará cuando se complete."
|
msgstr "Esta página se actualizará cuando se complete."
|
||||||
|
|
||||||
|
|||||||
@@ -721,7 +721,7 @@ msgstr "DTSTART valide obligatoire dans rrule. La valeur doit commencer par : DT
|
|||||||
#: awx/api/serializers.py:4657
|
#: awx/api/serializers.py:4657
|
||||||
msgid ""
|
msgid ""
|
||||||
"DTSTART cannot be a naive datetime. Specify ;TZINFO= or YYYYMMDDTHHMMSSZZ."
|
"DTSTART cannot be a naive datetime. Specify ;TZINFO= or YYYYMMDDTHHMMSSZZ."
|
||||||
msgstr "DTSTART ne peut correspondre à une DateHeure naïve. Spécifier ;TZINFO= ou YYYYMMDDTHHMMSSZZ."
|
msgstr "DTSTART ne peut correspondre à une date-heure naïve. Spécifier ;TZINFO= ou YYYYMMDDTHHMMSSZZ."
|
||||||
|
|
||||||
#: awx/api/serializers.py:4659
|
#: awx/api/serializers.py:4659
|
||||||
msgid "Multiple DTSTART is not supported."
|
msgid "Multiple DTSTART is not supported."
|
||||||
@@ -6239,4 +6239,5 @@ msgstr "%s est en cours de mise à niveau."
|
|||||||
|
|
||||||
#: awx/ui/urls.py:24
|
#: awx/ui/urls.py:24
|
||||||
msgid "This page will refresh when complete."
|
msgid "This page will refresh when complete."
|
||||||
msgstr "Cette page sera rafraîchie une fois terminée."
|
msgstr "Cette page sera rafraîchie une fois terminée."
|
||||||
|
|
||||||
|
|||||||
@@ -6237,4 +6237,5 @@ msgstr "Er wordt momenteel een upgrade van%s geïnstalleerd."
|
|||||||
|
|
||||||
#: awx/ui/urls.py:24
|
#: awx/ui/urls.py:24
|
||||||
msgid "This page will refresh when complete."
|
msgid "This page will refresh when complete."
|
||||||
msgstr "Deze pagina wordt vernieuwd als hij klaar is."
|
msgstr "Deze pagina wordt vernieuwd als hij klaar is."
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -561,7 +561,6 @@ class NotificationAttachMixin(BaseAccess):
|
|||||||
|
|
||||||
|
|
||||||
class InstanceAccess(BaseAccess):
|
class InstanceAccess(BaseAccess):
|
||||||
|
|
||||||
model = Instance
|
model = Instance
|
||||||
prefetch_related = ('rampart_groups',)
|
prefetch_related = ('rampart_groups',)
|
||||||
|
|
||||||
@@ -579,7 +578,6 @@ class InstanceAccess(BaseAccess):
|
|||||||
return super(InstanceAccess, self).can_unattach(obj, sub_obj, relationship, relationship, data=data)
|
return super(InstanceAccess, self).can_unattach(obj, sub_obj, relationship, relationship, data=data)
|
||||||
|
|
||||||
def can_add(self, data):
|
def can_add(self, data):
|
||||||
|
|
||||||
return self.user.is_superuser
|
return self.user.is_superuser
|
||||||
|
|
||||||
def can_change(self, obj, data):
|
def can_change(self, obj, data):
|
||||||
@@ -590,7 +588,6 @@ class InstanceAccess(BaseAccess):
|
|||||||
|
|
||||||
|
|
||||||
class InstanceGroupAccess(BaseAccess):
|
class InstanceGroupAccess(BaseAccess):
|
||||||
|
|
||||||
model = InstanceGroup
|
model = InstanceGroup
|
||||||
prefetch_related = ('instances',)
|
prefetch_related = ('instances',)
|
||||||
|
|
||||||
@@ -1030,7 +1027,9 @@ class GroupAccess(BaseAccess):
|
|||||||
return Group.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
|
return Group.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
|
||||||
|
|
||||||
def can_add(self, data):
|
def can_add(self, data):
|
||||||
if not data or 'inventory' not in data:
|
if not data: # So the browseable API will work
|
||||||
|
return Inventory.accessible_objects(self.user, 'admin_role').exists()
|
||||||
|
if 'inventory' not in data:
|
||||||
return False
|
return False
|
||||||
# Checks for admin or change permission on inventory.
|
# Checks for admin or change permission on inventory.
|
||||||
return self.check_related('inventory', Inventory, data)
|
return self.check_related('inventory', Inventory, data)
|
||||||
@@ -2352,7 +2351,6 @@ class JobEventAccess(BaseAccess):
|
|||||||
|
|
||||||
|
|
||||||
class UnpartitionedJobEventAccess(JobEventAccess):
|
class UnpartitionedJobEventAccess(JobEventAccess):
|
||||||
|
|
||||||
model = UnpartitionedJobEvent
|
model = UnpartitionedJobEvent
|
||||||
|
|
||||||
|
|
||||||
@@ -2697,46 +2695,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
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import aioredis
|
|
||||||
import redis
|
import redis
|
||||||
|
import redis.asyncio
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from prometheus_client import (
|
from prometheus_client import (
|
||||||
@@ -82,7 +82,7 @@ class BroadcastWebsocketStatsManager:
|
|||||||
|
|
||||||
async def run_loop(self):
|
async def run_loop(self):
|
||||||
try:
|
try:
|
||||||
redis_conn = await aioredis.create_redis_pool(settings.BROKER_URL)
|
redis_conn = await redis.asyncio.Redis.from_url(settings.BROKER_URL)
|
||||||
while True:
|
while True:
|
||||||
stats_data_str = ''.join(stat.serialize() for stat in self._stats.values())
|
stats_data_str = ''.join(stat.serialize() for stat in self._stats.values())
|
||||||
await redis_conn.set(self._redis_key, stats_data_str)
|
await redis_conn.set(self._redis_key, stats_data_str)
|
||||||
@@ -122,8 +122,8 @@ class BroadcastWebsocketStats:
|
|||||||
'Number of messages received, to be forwarded, by the broadcast websocket system',
|
'Number of messages received, to be forwarded, by the broadcast websocket system',
|
||||||
registry=self._registry,
|
registry=self._registry,
|
||||||
)
|
)
|
||||||
self._messages_received = Gauge(
|
self._messages_received_current_conn = Gauge(
|
||||||
f'awx_{self.remote_name}_messages_received',
|
f'awx_{self.remote_name}_messages_received_currrent_conn',
|
||||||
'Number forwarded messages received by the broadcast websocket system, for the duration of the current connection',
|
'Number forwarded messages received by the broadcast websocket system, for the duration of the current connection',
|
||||||
registry=self._registry,
|
registry=self._registry,
|
||||||
)
|
)
|
||||||
@@ -144,13 +144,13 @@ class BroadcastWebsocketStats:
|
|||||||
|
|
||||||
def record_message_received(self):
|
def record_message_received(self):
|
||||||
self._internal_messages_received_per_minute.record()
|
self._internal_messages_received_per_minute.record()
|
||||||
self._messages_received.inc()
|
self._messages_received_current_conn.inc()
|
||||||
self._messages_received_total.inc()
|
self._messages_received_total.inc()
|
||||||
|
|
||||||
def record_connection_established(self):
|
def record_connection_established(self):
|
||||||
self._connection.state('connected')
|
self._connection.state('connected')
|
||||||
self._connection_start.set_to_current_time()
|
self._connection_start.set_to_current_time()
|
||||||
self._messages_received.set(0)
|
self._messages_received_current_conn.set(0)
|
||||||
|
|
||||||
def record_connection_lost(self):
|
def record_connection_lost(self):
|
||||||
self._connection.state('disconnected')
|
self._connection.state('disconnected')
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from awx.conf.license import get_license
|
|||||||
from awx.main.utils import get_awx_version, camelcase_to_underscore, datetime_hook
|
from awx.main.utils import get_awx_version, camelcase_to_underscore, datetime_hook
|
||||||
from awx.main import models
|
from awx.main import models
|
||||||
from awx.main.analytics import register
|
from awx.main.analytics import register
|
||||||
from awx.main.scheduler.task_manager_models import TaskManagerInstances
|
from awx.main.scheduler.task_manager_models import TaskManagerModels
|
||||||
|
|
||||||
"""
|
"""
|
||||||
This module is used to define metrics collected by awx.main.analytics.gather()
|
This module is used to define metrics collected by awx.main.analytics.gather()
|
||||||
@@ -237,11 +237,8 @@ def projects_by_scm_type(since, **kwargs):
|
|||||||
def instance_info(since, include_hostnames=False, **kwargs):
|
def instance_info(since, include_hostnames=False, **kwargs):
|
||||||
info = {}
|
info = {}
|
||||||
# Use same method that the TaskManager does to compute consumed capacity without querying all running jobs for each Instance
|
# Use same method that the TaskManager does to compute consumed capacity without querying all running jobs for each Instance
|
||||||
active_tasks = models.UnifiedJob.objects.filter(status__in=['running', 'waiting']).only('task_impact', 'controller_node', 'execution_node')
|
tm_models = TaskManagerModels.init_with_consumed_capacity(instance_fields=['uuid', 'version', 'capacity', 'cpu', 'memory', 'managed_by_policy', 'enabled'])
|
||||||
tm_instances = TaskManagerInstances(
|
for tm_instance in tm_models.instances.instances_by_hostname.values():
|
||||||
active_tasks, instance_fields=['uuid', 'version', 'capacity', 'cpu', 'memory', 'managed_by_policy', 'enabled', 'node_type']
|
|
||||||
)
|
|
||||||
for tm_instance in tm_instances.instances_by_hostname.values():
|
|
||||||
instance = tm_instance.obj
|
instance = tm_instance.obj
|
||||||
instance_info = {
|
instance_info = {
|
||||||
'uuid': instance.uuid,
|
'uuid': instance.uuid,
|
||||||
|
|||||||
@@ -3,6 +3,5 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
|
|
||||||
class MainConfig(AppConfig):
|
class MainConfig(AppConfig):
|
||||||
|
|
||||||
name = 'awx.main'
|
name = 'awx.main'
|
||||||
verbose_name = _('Main')
|
verbose_name = _('Main')
|
||||||
|
|||||||
@@ -569,7 +569,7 @@ register(
|
|||||||
register(
|
register(
|
||||||
'LOG_AGGREGATOR_LOGGERS',
|
'LOG_AGGREGATOR_LOGGERS',
|
||||||
field_class=fields.StringListField,
|
field_class=fields.StringListField,
|
||||||
default=['awx', 'activity_stream', 'job_events', 'system_tracking'],
|
default=['awx', 'activity_stream', 'job_events', 'system_tracking', 'broadcast_websocket'],
|
||||||
label=_('Loggers Sending Data to Log Aggregator Form'),
|
label=_('Loggers Sending Data to Log Aggregator Form'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
'List of loggers that will send HTTP logs to the collector, these can '
|
'List of loggers that will send HTTP logs to the collector, these can '
|
||||||
@@ -577,7 +577,8 @@ register(
|
|||||||
'awx - service logs\n'
|
'awx - service logs\n'
|
||||||
'activity_stream - activity stream records\n'
|
'activity_stream - activity stream records\n'
|
||||||
'job_events - callback data from Ansible job events\n'
|
'job_events - callback data from Ansible job events\n'
|
||||||
'system_tracking - facts gathered from scan jobs.'
|
'system_tracking - facts gathered from scan jobs\n'
|
||||||
|
'broadcast_websocket - errors pertaining to websockets broadcast metrics\n'
|
||||||
),
|
),
|
||||||
category=_('Logging'),
|
category=_('Logging'),
|
||||||
category_slug='logging',
|
category_slug='logging',
|
||||||
|
|||||||
@@ -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.get('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)
|
||||||
|
|||||||
@@ -68,7 +68,11 @@ def conjur_backend(**kwargs):
|
|||||||
with CertFiles(cacert) as cert:
|
with CertFiles(cacert) as cert:
|
||||||
# https://www.conjur.org/api.html#authentication-authenticate-post
|
# https://www.conjur.org/api.html#authentication-authenticate-post
|
||||||
auth_kwargs['verify'] = cert
|
auth_kwargs['verify'] = cert
|
||||||
resp = requests.post(urljoin(url, '/'.join(['api', 'authn', account, username, 'authenticate'])), **auth_kwargs)
|
try:
|
||||||
|
resp = requests.post(urljoin(url, '/'.join(['authn', account, username, 'authenticate'])), **auth_kwargs)
|
||||||
|
resp.raise_for_status()
|
||||||
|
except requests.exceptions.HTTPError:
|
||||||
|
resp = requests.post(urljoin(url, '/'.join(['api', 'authn', account, username, 'authenticate'])), **auth_kwargs)
|
||||||
raise_for_status(resp)
|
raise_for_status(resp)
|
||||||
token = resp.content.decode('utf-8')
|
token = resp.content.decode('utf-8')
|
||||||
|
|
||||||
@@ -78,14 +82,20 @@ def conjur_backend(**kwargs):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# https://www.conjur.org/api.html#secrets-retrieve-a-secret-get
|
# https://www.conjur.org/api.html#secrets-retrieve-a-secret-get
|
||||||
path = urljoin(url, '/'.join(['api', 'secrets', account, 'variable', secret_path]))
|
path = urljoin(url, '/'.join(['secrets', account, 'variable', secret_path]))
|
||||||
|
path_conjurcloud = urljoin(url, '/'.join(['api', 'secrets', account, 'variable', secret_path]))
|
||||||
if version:
|
if version:
|
||||||
ver = "version={}".format(version)
|
ver = "version={}".format(version)
|
||||||
path = '?'.join([path, ver])
|
path = '?'.join([path, ver])
|
||||||
|
path_conjurcloud = '?'.join([path_conjurcloud, ver])
|
||||||
|
|
||||||
with CertFiles(cacert) as cert:
|
with CertFiles(cacert) as cert:
|
||||||
lookup_kwargs['verify'] = cert
|
lookup_kwargs['verify'] = cert
|
||||||
resp = requests.get(path, timeout=30, **lookup_kwargs)
|
try:
|
||||||
|
resp = requests.get(path, timeout=30, **lookup_kwargs)
|
||||||
|
resp.raise_for_status()
|
||||||
|
except requests.exceptions.HTTPError:
|
||||||
|
resp = requests.get(path_conjurcloud, timeout=30, **lookup_kwargs)
|
||||||
raise_for_status(resp)
|
raise_for_status(resp)
|
||||||
return resp.text
|
return resp.text
|
||||||
|
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ logger = logging.getLogger('awx.main.dispatch')
|
|||||||
|
|
||||||
|
|
||||||
class Control(object):
|
class Control(object):
|
||||||
|
|
||||||
services = ('dispatcher', 'callback_receiver')
|
services = ('dispatcher', 'callback_receiver')
|
||||||
result = None
|
result = None
|
||||||
|
|
||||||
|
|||||||
@@ -192,7 +192,6 @@ class PoolWorker(object):
|
|||||||
|
|
||||||
|
|
||||||
class StatefulPoolWorker(PoolWorker):
|
class StatefulPoolWorker(PoolWorker):
|
||||||
|
|
||||||
track_managed_tasks = True
|
track_managed_tasks = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ class task:
|
|||||||
bind_kwargs = self.bind_kwargs
|
bind_kwargs = self.bind_kwargs
|
||||||
|
|
||||||
class PublisherMixin(object):
|
class PublisherMixin(object):
|
||||||
|
|
||||||
queue = None
|
queue = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ class WorkerSignalHandler:
|
|||||||
|
|
||||||
|
|
||||||
class AWXConsumerBase(object):
|
class AWXConsumerBase(object):
|
||||||
|
|
||||||
last_stats = time.time()
|
last_stats = time.time()
|
||||||
|
|
||||||
def __init__(self, name, worker, queues=[], pool=None):
|
def __init__(self, name, worker, queues=[], pool=None):
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -155,6 +154,8 @@ class CallbackBrokerWorker(BaseWorker):
|
|||||||
metrics_events_missing_created = 0
|
metrics_events_missing_created = 0
|
||||||
metrics_total_job_event_processing_seconds = datetime.timedelta(seconds=0)
|
metrics_total_job_event_processing_seconds = datetime.timedelta(seconds=0)
|
||||||
for cls, events in self.buff.items():
|
for cls, events in self.buff.items():
|
||||||
|
if not events:
|
||||||
|
continue
|
||||||
logger.debug(f'{cls.__name__}.objects.bulk_create({len(events)})')
|
logger.debug(f'{cls.__name__}.objects.bulk_create({len(events)})')
|
||||||
for e in events:
|
for e in events:
|
||||||
e.modified = now # this can be set before created because now is set above on line 149
|
e.modified = now # this can be set before created because now is set above on line 149
|
||||||
@@ -164,38 +165,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 +278,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))
|
|
||||||
|
|||||||
@@ -232,7 +232,6 @@ class ImplicitRoleField(models.ForeignKey):
|
|||||||
field_names = [field_names]
|
field_names = [field_names]
|
||||||
|
|
||||||
for field_name in field_names:
|
for field_name in field_names:
|
||||||
|
|
||||||
if field_name.startswith('singleton:'):
|
if field_name.startswith('singleton:'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -244,7 +243,6 @@ class ImplicitRoleField(models.ForeignKey):
|
|||||||
field = getattr(cls, field_name, None)
|
field = getattr(cls, field_name, None)
|
||||||
|
|
||||||
if field and type(field) is ReverseManyToOneDescriptor or type(field) is ManyToManyDescriptor:
|
if field and type(field) is ReverseManyToOneDescriptor or type(field) is ManyToManyDescriptor:
|
||||||
|
|
||||||
if '.' in field_attr:
|
if '.' in field_attr:
|
||||||
raise Exception('Referencing deep roles through ManyToMany fields is unsupported.')
|
raise Exception('Referencing deep roles through ManyToMany fields is unsupported.')
|
||||||
|
|
||||||
@@ -629,7 +627,6 @@ class CredentialInputField(JSONSchemaField):
|
|||||||
# `ssh_key_unlock` requirements are very specific and can't be
|
# `ssh_key_unlock` requirements are very specific and can't be
|
||||||
# represented without complicated JSON schema
|
# represented without complicated JSON schema
|
||||||
if model_instance.credential_type.managed is True and 'ssh_key_unlock' in defined_fields:
|
if model_instance.credential_type.managed is True and 'ssh_key_unlock' in defined_fields:
|
||||||
|
|
||||||
# in order to properly test the necessity of `ssh_key_unlock`, we
|
# in order to properly test the necessity of `ssh_key_unlock`, we
|
||||||
# need to know the real value of `ssh_key_data`; for a payload like:
|
# need to know the real value of `ssh_key_data`; for a payload like:
|
||||||
# {
|
# {
|
||||||
@@ -791,7 +788,8 @@ class CredentialTypeInjectorField(JSONSchemaField):
|
|||||||
'type': 'object',
|
'type': 'object',
|
||||||
'patternProperties': {
|
'patternProperties': {
|
||||||
# http://docs.ansible.com/ansible/playbooks_variables.html#what-makes-a-valid-variable-name
|
# http://docs.ansible.com/ansible/playbooks_variables.html#what-makes-a-valid-variable-name
|
||||||
'^[a-zA-Z_]+[a-zA-Z0-9_]*$': {'type': 'string'},
|
# plus, add ability to template
|
||||||
|
r'^[a-zA-Z_\{\}]+[a-zA-Z0-9_\{\}]*$': {"anyOf": [{'type': 'string'}, {'type': 'array'}, {'$ref': '#/properties/extra_vars'}]}
|
||||||
},
|
},
|
||||||
'additionalProperties': False,
|
'additionalProperties': False,
|
||||||
},
|
},
|
||||||
@@ -858,27 +856,44 @@ class CredentialTypeInjectorField(JSONSchemaField):
|
|||||||
template_name = template_name.split('.')[1]
|
template_name = template_name.split('.')[1]
|
||||||
setattr(valid_namespace['tower'].filename, template_name, 'EXAMPLE_FILENAME')
|
setattr(valid_namespace['tower'].filename, template_name, 'EXAMPLE_FILENAME')
|
||||||
|
|
||||||
|
def validate_template_string(type_, key, tmpl):
|
||||||
|
try:
|
||||||
|
sandbox.ImmutableSandboxedEnvironment(undefined=StrictUndefined).from_string(tmpl).render(valid_namespace)
|
||||||
|
except UndefinedError as e:
|
||||||
|
raise django_exceptions.ValidationError(
|
||||||
|
_('{sub_key} uses an undefined field ({error_msg})').format(sub_key=key, error_msg=e),
|
||||||
|
code='invalid',
|
||||||
|
params={'value': value},
|
||||||
|
)
|
||||||
|
except SecurityError as e:
|
||||||
|
raise django_exceptions.ValidationError(_('Encountered unsafe code execution: {}').format(e))
|
||||||
|
except TemplateSyntaxError as e:
|
||||||
|
raise django_exceptions.ValidationError(
|
||||||
|
_('Syntax error rendering template for {sub_key} inside of {type} ({error_msg})').format(sub_key=key, type=type_, error_msg=e),
|
||||||
|
code='invalid',
|
||||||
|
params={'value': value},
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_extra_vars(key, node):
|
||||||
|
if isinstance(node, dict):
|
||||||
|
for k, v in node.items():
|
||||||
|
validate_template_string("extra_vars", 'a key' if key is None else key, k)
|
||||||
|
validate_extra_vars(k if key is None else "{key}.{k}".format(key=key, k=k), v)
|
||||||
|
elif isinstance(node, list):
|
||||||
|
for i, x in enumerate(node):
|
||||||
|
validate_extra_vars("{key}[{i}]".format(key=key, i=i), x)
|
||||||
|
else:
|
||||||
|
validate_template_string("extra_vars", key, node)
|
||||||
|
|
||||||
for type_, injector in value.items():
|
for type_, injector in value.items():
|
||||||
if type_ == 'env':
|
if type_ == 'env':
|
||||||
for key in injector.keys():
|
for key in injector.keys():
|
||||||
self.validate_env_var_allowed(key)
|
self.validate_env_var_allowed(key)
|
||||||
for key, tmpl in injector.items():
|
if type_ == 'extra_vars':
|
||||||
try:
|
validate_extra_vars(None, injector)
|
||||||
sandbox.ImmutableSandboxedEnvironment(undefined=StrictUndefined).from_string(tmpl).render(valid_namespace)
|
else:
|
||||||
except UndefinedError as e:
|
for key, tmpl in injector.items():
|
||||||
raise django_exceptions.ValidationError(
|
validate_template_string(type_, key, tmpl)
|
||||||
_('{sub_key} uses an undefined field ({error_msg})').format(sub_key=key, error_msg=e),
|
|
||||||
code='invalid',
|
|
||||||
params={'value': value},
|
|
||||||
)
|
|
||||||
except SecurityError as e:
|
|
||||||
raise django_exceptions.ValidationError(_('Encountered unsafe code execution: {}').format(e))
|
|
||||||
except TemplateSyntaxError as e:
|
|
||||||
raise django_exceptions.ValidationError(
|
|
||||||
_('Syntax error rendering template for {sub_key} inside of {type} ({error_msg})').format(sub_key=key, type=type_, error_msg=e),
|
|
||||||
code='invalid',
|
|
||||||
params={'value': value},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AskForField(models.BooleanField):
|
class AskForField(models.BooleanField):
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ class Command(BaseCommand):
|
|||||||
"""Checks connection to the database, and prints out connection info if not connected"""
|
"""Checks connection to the database, and prints out connection info if not connected"""
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute("SELECT version()")
|
cursor.execute("SELECT version()")
|
||||||
version = str(cursor.fetchone()[0])
|
version = str(cursor.fetchone()[0])
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ class DeleteMeta:
|
|||||||
part_drop = {}
|
part_drop = {}
|
||||||
|
|
||||||
for pk, status, created in self.jobs_qs:
|
for pk, status, created in self.jobs_qs:
|
||||||
|
|
||||||
part_key = partition_table_name(self.job_class, created)
|
part_key = partition_table_name(self.job_class, created)
|
||||||
if status in ['pending', 'waiting', 'running']:
|
if status in ['pending', 'waiting', 'running']:
|
||||||
part_drop[part_key] = False
|
part_drop[part_key] = False
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
if not options['user']:
|
if not options['user']:
|
||||||
|
|
||||||
raise CommandError('Username not supplied. Usage: awx-manage create_oauth2_token --user=username.')
|
raise CommandError('Username not supplied. Usage: awx-manage create_oauth2_token --user=username.')
|
||||||
try:
|
try:
|
||||||
user = User.objects.get(username=options['user'])
|
user = User.objects.get(username=options['user'])
|
||||||
|
|||||||
143
awx/main/management/commands/disable_instance.py
Normal file
143
awx/main/management/commands/disable_instance.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import time
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
from argparse import ArgumentTypeError
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from awx.main.models import Instance, UnifiedJob
|
||||||
|
|
||||||
|
|
||||||
|
class AWXInstance:
|
||||||
|
def __init__(self, **filter):
|
||||||
|
self.filter = filter
|
||||||
|
self.get_instance()
|
||||||
|
|
||||||
|
def get_instance(self):
|
||||||
|
filter = self.filter if self.filter is not None else dict(hostname=settings.CLUSTER_HOST_ID)
|
||||||
|
qs = Instance.objects.filter(**filter)
|
||||||
|
if not qs.exists():
|
||||||
|
raise ValueError(f"No AWX instance found with {filter} parameters")
|
||||||
|
self.instance = qs.first()
|
||||||
|
|
||||||
|
def disable(self):
|
||||||
|
if self.instance.enabled:
|
||||||
|
self.instance.enabled = False
|
||||||
|
self.instance.save()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def enable(self):
|
||||||
|
if not self.instance.enabled:
|
||||||
|
self.instance.enabled = True
|
||||||
|
self.instance.save()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def jobs(self):
|
||||||
|
return UnifiedJob.objects.filter(
|
||||||
|
Q(controller_node=self.instance.hostname) | Q(execution_node=self.instance.hostname), status__in=("running", "waiting")
|
||||||
|
)
|
||||||
|
|
||||||
|
def jobs_pretty(self):
|
||||||
|
jobs = []
|
||||||
|
for j in self.jobs():
|
||||||
|
job_started = j.started if j.started else now()
|
||||||
|
# similar calculation of `elapsed` as the corresponding serializer
|
||||||
|
# does
|
||||||
|
td = now() - job_started
|
||||||
|
elapsed = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / (10**6 * 1.0)
|
||||||
|
elapsed = float(elapsed)
|
||||||
|
details = dict(
|
||||||
|
name=j.name,
|
||||||
|
url=j.get_ui_url(),
|
||||||
|
elapsed=elapsed,
|
||||||
|
)
|
||||||
|
jobs.append(details)
|
||||||
|
|
||||||
|
jobs = sorted(jobs, reverse=True, key=lambda j: j["elapsed"])
|
||||||
|
|
||||||
|
return ", ".join([f"[\"{j['name']}\"]({j['url']})" for j in jobs])
|
||||||
|
|
||||||
|
def instance_pretty(self):
|
||||||
|
instance = (
|
||||||
|
self.instance.hostname,
|
||||||
|
urljoin(settings.TOWER_URL_BASE, f"/#/instances/{self.instance.pk}/details"),
|
||||||
|
)
|
||||||
|
return f"[\"{instance[0]}\"]({instance[1]})"
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Disable instance, optionally waiting for all its managed jobs to finish."
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ge_1(arg):
|
||||||
|
if arg == "inf":
|
||||||
|
return float("inf")
|
||||||
|
|
||||||
|
int_arg = int(arg)
|
||||||
|
if int_arg < 1:
|
||||||
|
raise ArgumentTypeError(f"The value must be a positive number >= 1. Provided: \"{arg}\"")
|
||||||
|
return int_arg
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
filter_group = parser.add_mutually_exclusive_group()
|
||||||
|
|
||||||
|
filter_group.add_argument(
|
||||||
|
"--hostname",
|
||||||
|
type=str,
|
||||||
|
default=settings.CLUSTER_HOST_ID,
|
||||||
|
help=f"{Instance.hostname.field.help_text} Defaults to the hostname of the machine where the Python interpreter is currently executing".strip(),
|
||||||
|
)
|
||||||
|
filter_group.add_argument("--id", type=self.ge_1, help=Instance.id.field.help_text)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--wait",
|
||||||
|
action="store_true",
|
||||||
|
help="Wait for jobs managed by the instance to finish. With default retry arguments waits ~1h",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--retry",
|
||||||
|
type=self.ge_1,
|
||||||
|
default=120,
|
||||||
|
help="Number of retries when waiting for jobs to finish. Default: 120. Also accepts \"inf\" to wait indefinitely",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--retry_sleep",
|
||||||
|
type=self.ge_1,
|
||||||
|
default=30,
|
||||||
|
help="Number of seconds to sleep before consequtive retries when waiting. Default: 30",
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
try:
|
||||||
|
filter = dict(id=options["id"]) if options["id"] is not None else dict(hostname=options["hostname"])
|
||||||
|
instance = AWXInstance(**filter)
|
||||||
|
except ValueError as e:
|
||||||
|
raise CommandError(e)
|
||||||
|
|
||||||
|
if instance.disable():
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"Instance {instance.instance_pretty()} has been disabled"))
|
||||||
|
else:
|
||||||
|
self.stdout.write(f"Instance {instance.instance_pretty()} has already been disabled")
|
||||||
|
|
||||||
|
if not options["wait"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
rc = 1
|
||||||
|
while instance.jobs().count() > 0:
|
||||||
|
if rc < options["retry"]:
|
||||||
|
self.stdout.write(
|
||||||
|
f"{rc}/{options['retry']}: Waiting {options['retry_sleep']}s before the next attempt to see if the following instance' managed jobs have finished: {instance.jobs_pretty()}"
|
||||||
|
)
|
||||||
|
rc += 1
|
||||||
|
time.sleep(options["retry_sleep"])
|
||||||
|
else:
|
||||||
|
raise CommandError(
|
||||||
|
f"{rc}/{options['retry']}: No more retry attempts left, but the instance still has associated managed jobs: {instance.jobs_pretty()}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.SUCCESS("Done waiting for instance' managed jobs to finish!"))
|
||||||
35
awx/main/management/commands/enable_local_authentication.py
Normal file
35
awx/main/management/commands/enable_local_authentication.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""enable or disable authentication system"""
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
"""
|
||||||
|
This adds the --enable --disable functionalities to the command using mutally_exclusive to avoid situations in which users pass both flags
|
||||||
|
"""
|
||||||
|
group = parser.add_mutually_exclusive_group()
|
||||||
|
group.add_argument('--enable', dest='enable', action='store_true', help='Pass --enable to enable local authentication')
|
||||||
|
group.add_argument('--disable', dest='disable', action='store_true', help='Pass --disable to disable local authentication')
|
||||||
|
|
||||||
|
def _enable_disable_auth(self, enable, disable):
|
||||||
|
"""
|
||||||
|
this method allows the disabling or enabling of local authenication based on the argument passed into the parser
|
||||||
|
if no arguments throw a command error, if --enable set the DISABLE_LOCAL_AUTH to False
|
||||||
|
if --disable it's set to True. Realizing that the flag is counterintuitive to what is expected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if enable:
|
||||||
|
settings.DISABLE_LOCAL_AUTH = False
|
||||||
|
print("Setting has changed to {} allowing local authentication".format(settings.DISABLE_LOCAL_AUTH))
|
||||||
|
|
||||||
|
elif disable:
|
||||||
|
settings.DISABLE_LOCAL_AUTH = True
|
||||||
|
print("Setting has changed to {} disallowing local authentication".format(settings.DISABLE_LOCAL_AUTH))
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise CommandError('Please pass --enable flag to allow local auth or --disable flag to disable local auth')
|
||||||
|
|
||||||
|
def handle(self, **options):
|
||||||
|
self._enable_disable_auth(options.get('enable'), options.get('disable'))
|
||||||
@@ -10,7 +10,6 @@ from django.utils.text import slugify
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
|
||||||
help = 'Export custom inventory scripts into a tarfile.'
|
help = 'Export custom inventory scripts into a tarfile.'
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
@@ -21,7 +20,6 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdirname:
|
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||||
with tarfile.open(tar_filename, "w") as tar:
|
with tarfile.open(tar_filename, "w") as tar:
|
||||||
|
|
||||||
for cis in CustomInventoryScript.objects.all():
|
for cis in CustomInventoryScript.objects.all():
|
||||||
# naming convention similar to project paths
|
# naming convention similar to project paths
|
||||||
slug_name = slugify(str(cis.name)).replace(u'-', u'_')
|
slug_name = slugify(str(cis.name)).replace(u'-', u'_')
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import json
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
|
||||||
help = 'This is for offline licensing usage'
|
help = 'This is for offline licensing usage'
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
|
|||||||
@@ -934,7 +934,6 @@ class Command(BaseCommand):
|
|||||||
# (even though inventory_import.Command.handle -- which calls
|
# (even though inventory_import.Command.handle -- which calls
|
||||||
# perform_update -- has its own lock, inventory_ID_import)
|
# perform_update -- has its own lock, inventory_ID_import)
|
||||||
with advisory_lock('inventory_{}_perform_update'.format(self.inventory.id)):
|
with advisory_lock('inventory_{}_perform_update'.format(self.inventory.id)):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.check_license()
|
self.check_license()
|
||||||
except PermissionDenied as e:
|
except PermissionDenied as e:
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from django.core.management.base import BaseCommand
|
|||||||
|
|
||||||
|
|
||||||
class Ungrouped(object):
|
class Ungrouped(object):
|
||||||
|
|
||||||
name = 'ungrouped'
|
name = 'ungrouped'
|
||||||
policy_instance_percentage = None
|
policy_instance_percentage = None
|
||||||
policy_instance_minimum = None
|
policy_instance_minimum = None
|
||||||
|
|||||||
@@ -38,7 +38,14 @@ class Command(BaseCommand):
|
|||||||
(changed, instance) = Instance.objects.register(ip_address=os.environ.get('MY_POD_IP'), node_type='control', uuid=settings.SYSTEM_UUID)
|
(changed, instance) = Instance.objects.register(ip_address=os.environ.get('MY_POD_IP'), node_type='control', uuid=settings.SYSTEM_UUID)
|
||||||
RegisterQueue(settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME, 100, 0, [], is_container_group=False).register()
|
RegisterQueue(settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME, 100, 0, [], is_container_group=False).register()
|
||||||
RegisterQueue(
|
RegisterQueue(
|
||||||
settings.DEFAULT_EXECUTION_QUEUE_NAME, 100, 0, [], is_container_group=True, pod_spec_override=settings.DEFAULT_EXECUTION_QUEUE_POD_SPEC_OVERRIDE
|
settings.DEFAULT_EXECUTION_QUEUE_NAME,
|
||||||
|
100,
|
||||||
|
0,
|
||||||
|
[],
|
||||||
|
is_container_group=True,
|
||||||
|
pod_spec_override=settings.DEFAULT_EXECUTION_QUEUE_POD_SPEC_OVERRIDE,
|
||||||
|
max_forks=settings.DEFAULT_EXECUTION_QUEUE_MAX_FORKS,
|
||||||
|
max_concurrent_jobs=settings.DEFAULT_EXECUTION_QUEUE_MAX_CONCURRENT_JOBS,
|
||||||
).register()
|
).register()
|
||||||
else:
|
else:
|
||||||
(changed, instance) = Instance.objects.register(hostname=hostname, node_type=node_type, uuid=uuid)
|
(changed, instance) = Instance.objects.register(hostname=hostname, node_type=node_type, uuid=uuid)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ class InstanceNotFound(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class RegisterQueue:
|
class RegisterQueue:
|
||||||
def __init__(self, queuename, instance_percent, inst_min, hostname_list, is_container_group=None, pod_spec_override=None):
|
def __init__(
|
||||||
|
self, queuename, instance_percent, inst_min, hostname_list, is_container_group=None, pod_spec_override=None, max_forks=None, max_concurrent_jobs=None
|
||||||
|
):
|
||||||
self.instance_not_found_err = None
|
self.instance_not_found_err = None
|
||||||
self.queuename = queuename
|
self.queuename = queuename
|
||||||
self.instance_percent = instance_percent
|
self.instance_percent = instance_percent
|
||||||
@@ -25,6 +27,8 @@ class RegisterQueue:
|
|||||||
self.hostname_list = hostname_list
|
self.hostname_list = hostname_list
|
||||||
self.is_container_group = is_container_group
|
self.is_container_group = is_container_group
|
||||||
self.pod_spec_override = pod_spec_override
|
self.pod_spec_override = pod_spec_override
|
||||||
|
self.max_forks = max_forks
|
||||||
|
self.max_concurrent_jobs = max_concurrent_jobs
|
||||||
|
|
||||||
def get_create_update_instance_group(self):
|
def get_create_update_instance_group(self):
|
||||||
created = False
|
created = False
|
||||||
@@ -45,6 +49,14 @@ class RegisterQueue:
|
|||||||
ig.pod_spec_override = self.pod_spec_override
|
ig.pod_spec_override = self.pod_spec_override
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
|
if self.max_forks and (ig.max_forks != self.max_forks):
|
||||||
|
ig.max_forks = self.max_forks
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if self.max_concurrent_jobs and (ig.max_concurrent_jobs != self.max_concurrent_jobs):
|
||||||
|
ig.max_concurrent_jobs = self.max_concurrent_jobs
|
||||||
|
changed = True
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
ig.save()
|
ig.save()
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from django.core.management.base import BaseCommand, CommandError
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
|
||||||
help = (
|
help = (
|
||||||
"Remove an instance (specified by --hostname) from the specified queue (instance group).\n"
|
"Remove an instance (specified by --hostname) from the specified queue (instance group).\n"
|
||||||
"In order remove the queue, use the `unregister_queue` command."
|
"In order remove the queue, use the `unregister_queue` command."
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ class JobStatusLifeCycle:
|
|||||||
|
|
||||||
|
|
||||||
class ReplayJobEvents(JobStatusLifeCycle):
|
class ReplayJobEvents(JobStatusLifeCycle):
|
||||||
|
|
||||||
recording_start = None
|
recording_start = None
|
||||||
replay_start = None
|
replay_start = None
|
||||||
|
|
||||||
@@ -190,7 +189,6 @@ class ReplayJobEvents(JobStatusLifeCycle):
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
|
||||||
help = 'Replay job events over websockets ordered by created on date.'
|
help = 'Replay job events over websockets ordered by created on date.'
|
||||||
|
|
||||||
def _parse_slice_range(self, slice_arg):
|
def _parse_slice_range(self, slice_arg):
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from awx.main.models import CredentialType
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
|
||||||
help = 'Load default managed credential types.'
|
help = 'Load default managed credential types.'
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from django.core.management.base import BaseCommand, CommandError
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
|
||||||
help = (
|
help = (
|
||||||
"Remove specified queue (instance group) from database.\n"
|
"Remove specified queue (instance group) from database.\n"
|
||||||
"Instances inside of queue will continue to exist, \n"
|
"Instances inside of queue will continue to exist, \n"
|
||||||
|
|||||||
@@ -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 = {}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ class SettingsCacheMiddleware(MiddlewareMixin):
|
|||||||
|
|
||||||
|
|
||||||
class TimingMiddleware(threading.local, MiddlewareMixin):
|
class TimingMiddleware(threading.local, MiddlewareMixin):
|
||||||
|
|
||||||
dest = '/var/log/tower/profile'
|
dest = '/var/log/tower/profile'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user