mirror of
https://github.com/ansible/awx.git
synced 2026-02-07 20:44:45 -03:30
Compare commits
439 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d58a66d587 | ||
|
|
2c5d3ff64a | ||
|
|
f953e6ab55 | ||
|
|
99e417d748 | ||
|
|
1c0dc1cccb | ||
|
|
6067fc36f6 | ||
|
|
0c6be1f4d7 | ||
|
|
57732d4630 | ||
|
|
3af31df2c9 | ||
|
|
00ea453ccd | ||
|
|
27676b0634 | ||
|
|
2a9804e589 | ||
|
|
b990271dec | ||
|
|
76ba363513 | ||
|
|
bd6cac966a | ||
|
|
15c4ab3a9b | ||
|
|
820c0782ad | ||
|
|
b11908ed1f | ||
|
|
e20ea5edba | ||
|
|
3d08754be5 | ||
|
|
164d4e6419 | ||
|
|
d5ab54977c | ||
|
|
490746fe8c | ||
|
|
def79de72b | ||
|
|
f2fa28e9dc | ||
|
|
fed02ef14a | ||
|
|
1077e218bd | ||
|
|
76f75f1745 | ||
|
|
1bb597bba4 | ||
|
|
a0e5e74cab | ||
|
|
d935e3c5a3 | ||
|
|
460ce7fcfa | ||
|
|
ec8725d54f | ||
|
|
d8af8baae3 | ||
|
|
d36999acc7 | ||
|
|
5b8cfa353f | ||
|
|
07e86adbb7 | ||
|
|
1ad623f253 | ||
|
|
c0a0e16ba0 | ||
|
|
91d3f954cd | ||
|
|
b5f87b1bac | ||
|
|
173628c709 | ||
|
|
2cabaf8727 | ||
|
|
d38b3afd21 | ||
|
|
162aab7272 | ||
|
|
dcaab9bc8b | ||
|
|
8f1c071acf | ||
|
|
efdee6ed1f | ||
|
|
750c22a150 | ||
|
|
b444d10369 | ||
|
|
2f81a1d803 | ||
|
|
902566ccfb | ||
|
|
dd828cba26 | ||
|
|
45809abc6f | ||
|
|
3bcad7be81 | ||
|
|
21a2b1a630 | ||
|
|
0d75e1540c | ||
|
|
d43ea41b7f | ||
|
|
aca1e73d84 | ||
|
|
6e75d25b74 | ||
|
|
ccecd90015 | ||
|
|
5c400b96fe | ||
|
|
07160d9719 | ||
|
|
a4dd20872c | ||
|
|
0de900a021 | ||
|
|
b35aa1ac83 | ||
|
|
9b06fb1e6a | ||
|
|
24acacbcb6 | ||
|
|
98e44de7a1 | ||
|
|
b11f2f017f | ||
|
|
9d8a177c75 | ||
|
|
7d8963388f | ||
|
|
3e0d0ec62d | ||
|
|
a34953cd6c | ||
|
|
48b29352d7 | ||
|
|
8addc6bb59 | ||
|
|
76b25d8c96 | ||
|
|
e343c9698d | ||
|
|
bc02321689 | ||
|
|
e917cffb5f | ||
|
|
b4c3da55d2 | ||
|
|
b1481ec6e3 | ||
|
|
3b597cd80a | ||
|
|
b009b01251 | ||
|
|
2409bc5ee3 | ||
|
|
f775ed3f77 | ||
|
|
d373cbdca9 | ||
|
|
daaa0c8efe | ||
|
|
c6eb8cf59b | ||
|
|
8e27e0ce28 | ||
|
|
a070d57080 | ||
|
|
721c8d98f8 | ||
|
|
196368d89b | ||
|
|
8b2c98ffde | ||
|
|
1faaba4e6c | ||
|
|
7938bf58d4 | ||
|
|
fe176a4f8c | ||
|
|
7e8d0b5883 | ||
|
|
91594a1ae8 | ||
|
|
e6e79227e5 | ||
|
|
b03b12163f | ||
|
|
1c08206792 | ||
|
|
3f0a8eb5bc | ||
|
|
95cf6aa6d0 | ||
|
|
c2b1209cf3 | ||
|
|
7bec29bac7 | ||
|
|
053b7caf3a | ||
|
|
6b867e7d97 | ||
|
|
708244881c | ||
|
|
967f3cc6a7 | ||
|
|
e1902b6fe1 | ||
|
|
36316ae9a5 | ||
|
|
8e4edca85d | ||
|
|
30a499b785 | ||
|
|
68c62d74f6 | ||
|
|
847d6c65bd | ||
|
|
f29e7b9c81 | ||
|
|
32831794ee | ||
|
|
6b82ae46bc | ||
|
|
26660fa82e | ||
|
|
45286ebf41 | ||
|
|
71d3fd3dbc | ||
|
|
6527d23b42 | ||
|
|
1b767daaf6 | ||
|
|
860183f178 | ||
|
|
3e753a7315 | ||
|
|
606296f68b | ||
|
|
06fd131637 | ||
|
|
cf6c36ecab | ||
|
|
fc9f1a3518 | ||
|
|
ab20b5bd10 | ||
|
|
0e8f30a4a2 | ||
|
|
ddb8c93f17 | ||
|
|
425c70826f | ||
|
|
8195bd7386 | ||
|
|
300d54864d | ||
|
|
fefab89815 | ||
|
|
72848fa097 | ||
|
|
85ec3dd5c4 | ||
|
|
98de4cb35f | ||
|
|
dab7889715 | ||
|
|
95e69124af | ||
|
|
1f4c46d873 | ||
|
|
d272ee3521 | ||
|
|
bedbafe0f9 | ||
|
|
676491134d | ||
|
|
b36c5d9209 | ||
|
|
7296415fd9 | ||
|
|
00b3ff6e51 | ||
|
|
bfaa2abd31 | ||
|
|
34be08d38e | ||
|
|
2992a5f5a4 | ||
|
|
3803763c53 | ||
|
|
f0aec0d444 | ||
|
|
765595fd11 | ||
|
|
75c6c068a7 | ||
|
|
036780d337 | ||
|
|
a05aa20f24 | ||
|
|
4db3cd0326 | ||
|
|
2e07ac3c2c | ||
|
|
77717b6134 | ||
|
|
bf74aae80e | ||
|
|
496862592a | ||
|
|
ad536ec3d9 | ||
|
|
bc69406f31 | ||
|
|
959b81aec5 | ||
|
|
1a39cbc2f4 | ||
|
|
715dc4ece8 | ||
|
|
c44f8e9122 | ||
|
|
75a0382c31 | ||
|
|
0b221a42c0 | ||
|
|
6f43462689 | ||
|
|
d1a4f010b9 | ||
|
|
79b95883a3 | ||
|
|
f1e3b4f4f0 | ||
|
|
c410f1f344 | ||
|
|
310a0f88e5 | ||
|
|
4fca57dfd2 | ||
|
|
e89326dbf2 | ||
|
|
cd6df9299f | ||
|
|
80a6d4a29d | ||
|
|
222e58dd34 | ||
|
|
0be7d9b21a | ||
|
|
5a6c7a5788 | ||
|
|
3784f9515e | ||
|
|
6638d6c2bb | ||
|
|
afbdeb5ff8 | ||
|
|
381e9d2901 | ||
|
|
dd478af227 | ||
|
|
0424370d49 | ||
|
|
1a4bb42ac5 | ||
|
|
fdb008fb8c | ||
|
|
d793f0bc99 | ||
|
|
a9c16a6c90 | ||
|
|
34c23caed3 | ||
|
|
8239232d4d | ||
|
|
30ff112c87 | ||
|
|
f4454a6c93 | ||
|
|
44a3057d16 | ||
|
|
3c02265986 | ||
|
|
6715ea493f | ||
|
|
96ae3268a5 | ||
|
|
6d626b3793 | ||
|
|
350c585499 | ||
|
|
b76783791a | ||
|
|
981c9527b2 | ||
|
|
09dcb91c09 | ||
|
|
3ef07ee5f7 | ||
|
|
1edae24644 | ||
|
|
d4b0251845 | ||
|
|
ff90e64448 | ||
|
|
ab11d3188d | ||
|
|
f51774c06e | ||
|
|
83458711cd | ||
|
|
46734f5c10 | ||
|
|
b8b0812e13 | ||
|
|
38e0968690 | ||
|
|
1ec2bd7ed2 | ||
|
|
d10fdda453 | ||
|
|
2737cded72 | ||
|
|
4f2ce90137 | ||
|
|
d4305f734a | ||
|
|
347529a0cf | ||
|
|
e4470f1200 | ||
|
|
f225df1acd | ||
|
|
01f210e392 | ||
|
|
455e018099 | ||
|
|
4c98a1cb20 | ||
|
|
202a68aca0 | ||
|
|
29666bf3b6 | ||
|
|
ddbe20d41b | ||
|
|
140d9ae3ee | ||
|
|
84bea3d348 | ||
|
|
d0ac028265 | ||
|
|
7322e13436 | ||
|
|
9065b29bd6 | ||
|
|
dbdc856ed3 | ||
|
|
1cf2f009ed | ||
|
|
2bdd83e029 | ||
|
|
e91395d517 | ||
|
|
19115a157e | ||
|
|
a9d4046ec5 | ||
|
|
bbc4522063 | ||
|
|
fb3271da3c | ||
|
|
56d800e851 | ||
|
|
319308193c | ||
|
|
15ae0976dd | ||
|
|
f121dc59f6 | ||
|
|
f050b73560 | ||
|
|
bd624508e3 | ||
|
|
b9fb9389e6 | ||
|
|
bf28bdecb7 | ||
|
|
23cab61408 | ||
|
|
777d44ec34 | ||
|
|
bc14e995fd | ||
|
|
61d3a765ee | ||
|
|
f943277ef0 | ||
|
|
9a6da9cea7 | ||
|
|
8b4b1f5f81 | ||
|
|
5a96af79d4 | ||
|
|
b7f37d5e26 | ||
|
|
d14aee70a1 | ||
|
|
8aa11f4848 | ||
|
|
05799d9795 | ||
|
|
ea4b435ea7 | ||
|
|
2f1b4d81e1 | ||
|
|
a88f03b372 | ||
|
|
dbfb1780f1 | ||
|
|
a89eff3c1a | ||
|
|
2e3e1be22a | ||
|
|
a74fbd6578 | ||
|
|
803ea322d6 | ||
|
|
9657117941 | ||
|
|
b56c753613 | ||
|
|
8b3dfae717 | ||
|
|
8f50514b3d | ||
|
|
bdb97c173b | ||
|
|
24f86cd0d1 | ||
|
|
6e651ce275 | ||
|
|
650a97e989 | ||
|
|
1434e5812f | ||
|
|
e26387cdf3 | ||
|
|
7f81f530fa | ||
|
|
fab94902a0 | ||
|
|
e3e69b4c6b | ||
|
|
642803e471 | ||
|
|
26f2f159dd | ||
|
|
e962933148 | ||
|
|
169868b927 | ||
|
|
e0ebd5d46c | ||
|
|
d1f9f4dc86 | ||
|
|
5c12022529 | ||
|
|
d8f6ea3fe8 | ||
|
|
719a4f8aad | ||
|
|
7bac7abd67 | ||
|
|
776ee43d90 | ||
|
|
78229f5871 | ||
|
|
b4198204a7 | ||
|
|
86c1f4d772 | ||
|
|
e10fb71f93 | ||
|
|
bae50e38da | ||
|
|
66739a52d6 | ||
|
|
f060002289 | ||
|
|
b6883ab64e | ||
|
|
9dadb96d45 | ||
|
|
5304792949 | ||
|
|
a3e0ae66ba | ||
|
|
9705f7bec6 | ||
|
|
5610309a88 | ||
|
|
25fe090e67 | ||
|
|
a1f257bd4a | ||
|
|
638a6fdaa1 | ||
|
|
b4a6749699 | ||
|
|
e50576c820 | ||
|
|
0bedd6fbd8 | ||
|
|
48977e50df | ||
|
|
7aa8495d1a | ||
|
|
58b954df3e | ||
|
|
38079b2ad5 | ||
|
|
b055d34139 | ||
|
|
f2641de260 | ||
|
|
aa12e323b4 | ||
|
|
65a8a656f7 | ||
|
|
73bb539b16 | ||
|
|
6e99b1cf85 | ||
|
|
bde1ff1187 | ||
|
|
bd660254a5 | ||
|
|
879ab50a12 | ||
|
|
ad10f3581e | ||
|
|
eec2177315 | ||
|
|
f5d38f57d4 | ||
|
|
3f33f1c97d | ||
|
|
f9039703ee | ||
|
|
be33627755 | ||
|
|
c9cfaf65a0 | ||
|
|
12b87fca8c | ||
|
|
b5bbfaab11 | ||
|
|
8ea31d8cdd | ||
|
|
5ae7aa7503 | ||
|
|
c89277358f | ||
|
|
7e2b3684e2 | ||
|
|
c7d9d941e3 | ||
|
|
5b9c19df8f | ||
|
|
30757f8289 | ||
|
|
606d4e0ea2 | ||
|
|
1df91eece1 | ||
|
|
01b8087cb3 | ||
|
|
aef382862c | ||
|
|
41d8edf94a | ||
|
|
ddadf332cc | ||
|
|
12a22d2442 | ||
|
|
fa53cdf329 | ||
|
|
5445a3b567 | ||
|
|
2e37ffdb15 | ||
|
|
3b5640aca3 | ||
|
|
038688ca48 | ||
|
|
fb5d876b9b | ||
|
|
a62047809f | ||
|
|
b1fcc0578c | ||
|
|
dd42f6351b | ||
|
|
fad4b3ca5a | ||
|
|
ce65f994dc | ||
|
|
5532760ccf | ||
|
|
21ea7eb5dc | ||
|
|
b01ac1bfa6 | ||
|
|
f091de438e | ||
|
|
8a25f385e4 | ||
|
|
180ed4cd3d | ||
|
|
c1bbeadf8d | ||
|
|
b6c210ee97 | ||
|
|
f4514873cd | ||
|
|
2c59e83beb | ||
|
|
29da0abfc1 | ||
|
|
14b2bd3445 | ||
|
|
77ecb7b7d5 | ||
|
|
a9938c0e64 | ||
|
|
f24b15dc2e | ||
|
|
af199dff7a | ||
|
|
0dbaf632dc | ||
|
|
a7fe5e1dd2 | ||
|
|
96583e9e6b | ||
|
|
73ee11725d | ||
|
|
721ed516a4 | ||
|
|
4848f1298a | ||
|
|
6c6c1e8488 | ||
|
|
99b3997084 | ||
|
|
35fe255455 | ||
|
|
3cf4f4729d | ||
|
|
6376395c68 | ||
|
|
eaadbe9730 | ||
|
|
186c54cb14 | ||
|
|
40eb3e43f8 | ||
|
|
e1f7a7619f | ||
|
|
8eee0d40dd | ||
|
|
fe221c2473 | ||
|
|
9514adaf3a | ||
|
|
85deb8711c | ||
|
|
17eaeb28a8 | ||
|
|
7c1959010a | ||
|
|
1dd9772e41 | ||
|
|
18d09f892d | ||
|
|
ee6f03515e | ||
|
|
16f81154c0 | ||
|
|
1d7e2a82c6 | ||
|
|
b7ac5f0ffb | ||
|
|
0e5f68ef53 | ||
|
|
a057a2e056 | ||
|
|
e9e410f4f8 | ||
|
|
71cc359ccf | ||
|
|
37c1c33ecd | ||
|
|
2f7ba75ae4 | ||
|
|
9c20b9412a | ||
|
|
99c0a954e4 | ||
|
|
0a0735344e | ||
|
|
9ae344b772 | ||
|
|
0c4adfb32e | ||
|
|
4ce37ec849 | ||
|
|
c53e5bdbcf | ||
|
|
e0032f7705 | ||
|
|
ed05ce164f | ||
|
|
d98f2714cb | ||
|
|
85426f76a5 | ||
|
|
c48da1b384 | ||
|
|
aec7d3cc93 | ||
|
|
bd23c41d25 | ||
|
|
563d3944ed | ||
|
|
492d01ff3b | ||
|
|
3ded30411f | ||
|
|
b38c62d37f | ||
|
|
4c499b2d80 | ||
|
|
3d02bd7a90 | ||
|
|
71257c18c2 | ||
|
|
d85df2e4a1 | ||
|
|
59d457207c | ||
|
|
3ea642f212 | ||
|
|
b6d3c3c1a3 | ||
|
|
b83db0500f | ||
|
|
c4d9b81c55 | ||
|
|
cdfc9e05d4 |
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -3,6 +3,12 @@ name: "\U0001F41B Bug report"
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
<!-- Issues are for **concrete, actionable bugs and feature requests** only - if you're just asking for debugging help or technical support, please use:
|
||||
|
||||
- http://webchat.freenode.net/?channels=ansible-awx
|
||||
- https://groups.google.com/forum/#!forum/awx-project
|
||||
|
||||
We have to limit this because of limited volunteer time to respond to issues! -->
|
||||
|
||||
##### ISSUE TYPE
|
||||
- Bug Report
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -3,6 +3,12 @@ name: "✨ Feature request"
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
<!-- Issues are for **concrete, actionable bugs and feature requests** only - if you're just asking for debugging help or technical support, please use:
|
||||
|
||||
- http://webchat.freenode.net/?channels=ansible-awx
|
||||
- https://groups.google.com/forum/#!forum/awx-project
|
||||
|
||||
We have to limit this because of limited volunteer time to respond to issues! -->
|
||||
|
||||
##### ISSUE TYPE
|
||||
- Feature Idea
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -29,8 +29,10 @@ awx/ui/client/languages
|
||||
awx/ui/templates/ui/index.html
|
||||
awx/ui/templates/ui/installing.html
|
||||
awx/ui_next/node_modules/
|
||||
awx/ui_next/src/locales/
|
||||
awx/ui_next/coverage/
|
||||
awx/ui_next/build/locales/_build
|
||||
awx/ui_next/build
|
||||
awx/ui_next/.env.local
|
||||
rsyslog.pid
|
||||
/tower-license
|
||||
/tower-license/**
|
||||
@@ -139,8 +141,8 @@ use_dev_supervisor.txt
|
||||
# Ansible module tests
|
||||
/awx_collection_test_venv/
|
||||
/awx_collection/*.tar.gz
|
||||
/awx_collection/galaxy.yml
|
||||
/sanity/
|
||||
/awx_collection_build/
|
||||
|
||||
.idea/*
|
||||
*.unison.tmp
|
||||
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -2,8 +2,34 @@
|
||||
|
||||
This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/<version>`.
|
||||
|
||||
## 14.0.0 (Aug 6, 2020)
|
||||
- As part of our commitment to inclusivity in open source, we recently took some time to audit AWX's source code and user interface and replace certain terminology with more inclusive language. Strictly speaking, this isn't a bug or a feature, but we think it's important and worth calling attention to:
|
||||
* https://github.com/ansible/awx/commit/78229f58715fbfbf88177e54031f532543b57acc
|
||||
* https://www.redhat.com/en/blog/making-open-source-more-inclusive-eradicating-problematic-language
|
||||
- Installing roles and collections via requirements.yml as part of Project Updates now requires at least Ansible 2.9 - https://github.com/ansible/awx/issues/7769
|
||||
- Deprecated the use of the `PRIMARY_GALAXY_USERNAME` and `PRIMARY_GALAXY_PASSWORD` settings. We recommend using tokens to access Galaxy or Automation Hub.
|
||||
- Added local caching for downloaded roles and collections so they are not re-downloaded on nodes where they are up to date with the project - https://github.com/ansible/awx/issues/5518
|
||||
- Added the ability to associate K8S/OpenShift credentials to Job Template for playbook interaction with the `community.kubernetes` collection - https://github.com/ansible/awx/issues/5735
|
||||
- Added the ability to include HTML in the Custom Login Info presented on the login page - https://github.com/ansible/awx/issues/7600
|
||||
- Fixed https://access.redhat.com/security/cve/cve-2020-14327 - Server-side request forgery on credentials
|
||||
- Fixed https://access.redhat.com/security/cve/cve-2020-14328 - Server-side request forgery on webhooks
|
||||
- Fixed https://access.redhat.com/security/cve/cve-2020-14329 - Sensitive data exposure on labels
|
||||
- Fixed https://access.redhat.com/security/cve/cve-2020-14337 - Named URLs allow for testing the presence or absence of objects
|
||||
- Fixed a number of bugs in the user interface related to an upgrade of jQuery:
|
||||
* https://github.com/ansible/awx/issues/7530
|
||||
* https://github.com/ansible/awx/issues/7546
|
||||
* https://github.com/ansible/awx/issues/7534
|
||||
* https://github.com/ansible/awx/issues/7606
|
||||
- Fixed a bug that caused the `-f yaml` flag of the AWX CLI to not print properly formatted YAML - https://github.com/ansible/awx/issues/7795
|
||||
- Fixed a bug in the installer that caused errors when `docker_registry_password` was set - https://github.com/ansible/awx/issues/7695
|
||||
- Fixed a permissions error that prevented certain users from starting AWX services - https://github.com/ansible/awx/issues/7545
|
||||
- Fixed a bug that allows superusers to run unsafe Jinja code when defining custom Credential Types - https://github.com/ansible/awx/pull/7584/
|
||||
- Fixed a bug that prevented users from creating (or editing) custom Credential Types containing boolean fields - https://github.com/ansible/awx/issues/7483
|
||||
- Fixed a bug that prevented users with postgres usernames containing uppercase letters from restoring backups succesfully - https://github.com/ansible/awx/pull/7519
|
||||
- Fixed a bug which allowed the creation (in the Tower API) of Groups and Hosts with the same name - https://github.com/ansible/awx/issues/4680
|
||||
|
||||
## 13.0.0 (Jun 23, 2020)
|
||||
- Added import and export subcommands to the awx-cli tool, replacing send and receive from the old tower-cli (https://github.com/ansible/awx/pull/6125).
|
||||
- Added import and export commands to the official AWX CLI, replacing send and receive from the old tower-cli (https://github.com/ansible/awx/pull/6125).
|
||||
- Removed scripts as a means of running inventory updates of built-in types (https://github.com/ansible/awx/pull/6911)
|
||||
- Ansible 2.8 is now partially unsupported; some inventory source types are known to no longer work.
|
||||
- Fixed an issue where the vmware inventory source ssl_verify source variable was not recognized (https://github.com/ansible/awx/pull/7360)
|
||||
@@ -15,7 +41,7 @@ This is a list of high-level changes for each release of AWX. A full list of com
|
||||
- Moved to a single container image build instead of separate awx_web and awx_task images. The container image is just `awx` (https://github.com/ansible/awx/pull/7228)
|
||||
- Official AWX container image builds now use a two-stage container build process that notably reduces the size of our published images (https://github.com/ansible/awx/pull/7017)
|
||||
- Removed support for HipChat notifications ([EoL announcement](https://www.atlassian.com/partnerships/slack/faq#faq-98b17ca3-247f-423b-9a78-70a91681eff0)); all previously-created HipChat notification templates will be deleted due to this removal.
|
||||
- Fixed a bug which broke AWX installations with oc version 4.3 (https://github.com/ansible/awx/pull/6948/files)
|
||||
- Fixed a bug which broke AWX installations with oc version 4.3 (https://github.com/ansible/awx/pull/6948/)
|
||||
- Fixed a performance issue that caused notable delay of stdout processing for playbooks run against large numbers of hosts (https://github.com/ansible/awx/issues/6991)
|
||||
- Fixed a bug that caused CyberArk AIM credential plugin looks to hang forever in some environments (https://github.com/ansible/awx/issues/6986)
|
||||
- Fixed a bug that caused ANY/ALL converage settings not to properly save when editing approval nodes in the UI (https://github.com/ansible/awx/issues/6998)
|
||||
|
||||
20
INSTALL.md
20
INSTALL.md
@@ -43,7 +43,7 @@ This document provides a guide for installing AWX.
|
||||
- [Installing the AWX CLI](#installing-the-awx-cli)
|
||||
* [Building the CLI Documentation](#building-the-cli-documentation)
|
||||
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
### Clone the repo
|
||||
@@ -351,7 +351,7 @@ Once you access the AWX server, you will be prompted with a login dialog. The de
|
||||
A Kubernetes deployment will require you to have access to a Kubernetes cluster as well as the following tools:
|
||||
|
||||
- [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
|
||||
- [helm](https://docs.helm.sh/using_helm/#quickstart-guide)
|
||||
- [helm](https://helm.sh/docs/intro/quickstart/)
|
||||
|
||||
The installation program will reference `kubectl` directly. `helm` is only necessary if you are letting the installer configure PostgreSQL for you.
|
||||
|
||||
@@ -382,9 +382,11 @@ Before starting the install process, review the [inventory](./installer/inventor
|
||||
|
||||
### Configuring Helm
|
||||
|
||||
If you want the AWX installer to manage creating the database pod (rather than installing and configuring postgres on your own). Then you will need to have a working `helm` installation, you can find details here: [https://docs.helm.sh/using_helm/#quickstart-guide](https://docs.helm.sh/using_helm/#quickstart-guide).
|
||||
If you want the AWX installer to manage creating the database pod (rather than installing and configuring postgres on your own). Then you will need to have a working `helm` installation, you can find details here: [https://helm.sh/docs/intro/quickstart/](https://helm.sh/docs/intro/quickstart/).
|
||||
|
||||
Newer Kubernetes clusters with RBAC enabled will need to make sure a service account is created, make sure to follow the instructions here [https://docs.helm.sh/using_helm/#role-based-access-control](https://docs.helm.sh/using_helm/#role-based-access-control)
|
||||
You do not need to create a [Persistent Volume Claim](https://docs.openshift.org/latest/dev_guide/persistent_volumes.html) as Helm does it for you. However, an existing one may be used by setting the `pg_persistence_existingclaim` variable.
|
||||
|
||||
Newer Kubernetes clusters with RBAC enabled will need to make sure a service account is created, make sure to follow the instructions here [https://helm.sh/docs/topics/rbac/](https://helm.sh/docs/topics/rbac/)
|
||||
|
||||
### Run the installer
|
||||
|
||||
@@ -575,7 +577,7 @@ If you're deploying using Docker Compose, container names will be prefixed by th
|
||||
Immediately after the containers start, the *awx_task* container will perform required setup tasks, including database migrations. These tasks need to complete before the web interface can be accessed. To monitor the progress, you can follow the container's STDOUT by running the following:
|
||||
|
||||
```bash
|
||||
# Tail the the awx_task log
|
||||
# Tail the awx_task log
|
||||
$ docker logs -f awx_task
|
||||
```
|
||||
|
||||
@@ -651,16 +653,14 @@ Potential uses include:
|
||||
* Checking on the status and output of job runs
|
||||
* Managing objects like organizations, users, teams, etc...
|
||||
|
||||
The preferred way to install the AWX CLI is through pip directly from GitHub:
|
||||
The preferred way to install the AWX CLI is through pip directly from PyPI:
|
||||
|
||||
pip install "https://github.com/ansible/awx/archive/$VERSION.tar.gz#egg=awxkit&subdirectory=awxkit"
|
||||
pip3 install awxkit
|
||||
awx --help
|
||||
|
||||
...where ``$VERSION`` is the version of AWX you're running. To see a list of all available releases, visit: https://github.com/ansible/awx/releases
|
||||
|
||||
## Building the CLI Documentation
|
||||
|
||||
To build the docs, spin up a real AWX server, `pip install sphinx sphinxcontrib-autoprogram`, and run:
|
||||
To build the docs, spin up a real AWX server, `pip3 install sphinx sphinxcontrib-autoprogram`, and run:
|
||||
|
||||
~ TOWER_HOST=https://awx.example.org TOWER_USERNAME=example TOWER_PASSWORD=secret make clean html
|
||||
~ cd build/html/ && python -m http.server
|
||||
|
||||
@@ -31,7 +31,7 @@ If your issue isn't considered high priority, then please be patient as it may t
|
||||
|
||||
`state:needs_info` The issue needs more information. This could be more debug output, more specifics out the system such as version information. Any detail that is currently preventing this issue from moving forward. This should be considered a blocked state.
|
||||
|
||||
`state:needs_review` The the issue/pull request needs to be reviewed by other maintainers and contributors. This is usually used when there is a question out to another maintainer or when a person is less familar with an area of the code base the issue is for.
|
||||
`state:needs_review` The issue/pull request needs to be reviewed by other maintainers and contributors. This is usually used when there is a question out to another maintainer or when a person is less familar with an area of the code base the issue is for.
|
||||
|
||||
`state:needs_revision` More commonly used on pull requests, this state represents that there are changes that are being waited on.
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ recursive-include awx/templates *.html
|
||||
recursive-include awx/api/templates *.md *.html
|
||||
recursive-include awx/ui/templates *.html
|
||||
recursive-include awx/ui/static *
|
||||
recursive-include awx/ui_next/build *.html
|
||||
recursive-include awx/ui_next/build *
|
||||
recursive-include awx/playbooks *.yml
|
||||
recursive-include awx/lib/site-packages *
|
||||
recursive-include awx/plugins *.ps1
|
||||
|
||||
37
Makefile
37
Makefile
@@ -79,6 +79,7 @@ clean-ui: clean-languages
|
||||
rm -rf awx/ui/test/e2e/reports/
|
||||
rm -rf awx/ui/client/languages/
|
||||
rm -rf awx/ui_next/node_modules/
|
||||
rm -rf node_modules
|
||||
rm -rf awx/ui_next/coverage/
|
||||
rm -rf awx/ui_next/build/locales/_build/
|
||||
rm -f $(UI_DEPS_FLAG_FILE)
|
||||
@@ -368,7 +369,7 @@ test:
|
||||
PYTHONDONTWRITEBYTECODE=1 py.test -p no:cacheprovider -n auto $(TEST_DIRS)
|
||||
cmp VERSION awxkit/VERSION || "VERSION and awxkit/VERSION *must* match"
|
||||
cd awxkit && $(VENV_BASE)/awx/bin/tox -re py3
|
||||
awx-manage check_migrations --dry-run --check -n 'vNNN_missing_migration_file'
|
||||
awx-manage check_migrations --dry-run --check -n 'missing_migration_file'
|
||||
|
||||
COLLECTION_TEST_DIRS ?= awx_collection/test/awx
|
||||
COLLECTION_TEST_TARGET ?=
|
||||
@@ -401,11 +402,11 @@ symlink_collection:
|
||||
|
||||
build_collection:
|
||||
ansible-playbook -i localhost, awx_collection/tools/template_galaxy.yml -e collection_package=$(COLLECTION_PACKAGE) -e collection_namespace=$(COLLECTION_NAMESPACE) -e collection_version=$(VERSION) -e '{"awx_template_version":false}'
|
||||
ansible-galaxy collection build awx_collection --force --output-path=awx_collection
|
||||
ansible-galaxy collection build awx_collection_build --force --output-path=awx_collection_build
|
||||
|
||||
install_collection: build_collection
|
||||
rm -rf $(COLLECTION_INSTALL)
|
||||
ansible-galaxy collection install awx_collection/$(COLLECTION_NAMESPACE)-$(COLLECTION_PACKAGE)-$(VERSION).tar.gz
|
||||
ansible-galaxy collection install awx_collection_build/$(COLLECTION_NAMESPACE)-$(COLLECTION_PACKAGE)-$(VERSION).tar.gz
|
||||
|
||||
test_collection_sanity: install_collection
|
||||
cd $(COLLECTION_INSTALL) && ansible-test sanity
|
||||
@@ -568,14 +569,28 @@ ui-zuul-lint-and-test:
|
||||
# UI NEXT TASKS
|
||||
# --------------------------------------
|
||||
|
||||
ui-next-lint:
|
||||
awx/ui_next/node_modules:
|
||||
$(NPM_BIN) --prefix awx/ui_next install
|
||||
$(NPM_BIN) run --prefix awx/ui_next lint
|
||||
$(NPM_BIN) run --prefix awx/ui_next prettier-check
|
||||
|
||||
ui-next-test:
|
||||
$(NPM_BIN) --prefix awx/ui_next install
|
||||
$(NPM_BIN) run --prefix awx/ui_next test
|
||||
ui-release-next:
|
||||
mkdir -p awx/ui_next/build/static
|
||||
touch awx/ui_next/build/static/.placeholder
|
||||
|
||||
ui-devel-next: awx/ui_next/node_modules
|
||||
$(NPM_BIN) --prefix awx/ui_next run extract-strings
|
||||
$(NPM_BIN) --prefix awx/ui_next run compile-strings
|
||||
$(NPM_BIN) --prefix awx/ui_next run build
|
||||
mkdir -p awx/public/static/css
|
||||
mkdir -p awx/public/static/js
|
||||
mkdir -p awx/public/static/media
|
||||
cp -r awx/ui_next/build/static/css/* awx/public/static/css
|
||||
cp -r awx/ui_next/build/static/js/* awx/public/static/js
|
||||
cp -r awx/ui_next/build/static/media/* awx/public/static/media
|
||||
|
||||
clean-ui-next:
|
||||
rm -rf node_modules
|
||||
rm -rf awx/ui_next/node_modules
|
||||
rm -rf awx/ui_next/build
|
||||
|
||||
ui-next-zuul-lint-and-test:
|
||||
$(NPM_BIN) --prefix awx/ui_next install
|
||||
@@ -594,10 +609,10 @@ dev_build:
|
||||
release_build:
|
||||
$(PYTHON) setup.py release_build
|
||||
|
||||
dist/$(SDIST_TAR_FILE): ui-release VERSION
|
||||
dist/$(SDIST_TAR_FILE): ui-release ui-release-next VERSION
|
||||
$(PYTHON) setup.py $(SDIST_COMMAND)
|
||||
|
||||
dist/$(WHEEL_FILE): ui-release
|
||||
dist/$(WHEEL_FILE): ui-release ui-release-next
|
||||
$(PYTHON) setup.py $(WHEEL_COMMAND)
|
||||
|
||||
sdist: dist/$(SDIST_TAR_FILE)
|
||||
|
||||
@@ -146,7 +146,7 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
|
||||
# A list of fields that we know can be filtered on without the possiblity
|
||||
# of introducing duplicates
|
||||
NO_DUPLICATES_WHITELIST = (CharField, IntegerField, BooleanField)
|
||||
NO_DUPLICATES_ALLOW_LIST = (CharField, IntegerField, BooleanField)
|
||||
|
||||
def get_fields_from_lookup(self, model, lookup):
|
||||
|
||||
@@ -205,7 +205,7 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
field_list, new_lookup = self.get_fields_from_lookup(model, lookup)
|
||||
field = field_list[-1]
|
||||
|
||||
needs_distinct = (not all(isinstance(f, self.NO_DUPLICATES_WHITELIST) for f in field_list))
|
||||
needs_distinct = (not all(isinstance(f, self.NO_DUPLICATES_ALLOW_LIST) for f in field_list))
|
||||
|
||||
# Type names are stored without underscores internally, but are presented and
|
||||
# and serialized over the API containing underscores so we remove `_`
|
||||
@@ -257,6 +257,11 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
if key in self.RESERVED_NAMES:
|
||||
continue
|
||||
|
||||
# HACK: make `created` available via API for the Django User ORM model
|
||||
# so it keep compatiblity with other objects which exposes the `created` attr.
|
||||
if queryset.model._meta.object_name == 'User' and key.startswith('created'):
|
||||
key = key.replace('created', 'date_joined')
|
||||
|
||||
# HACK: Make job event filtering by host name mostly work even
|
||||
# when not capturing job event hosts M2M.
|
||||
if queryset.model._meta.object_name == 'JobEvent' and key.startswith('hosts__name'):
|
||||
|
||||
@@ -51,6 +51,7 @@ from awx.main.utils import (
|
||||
StubLicense
|
||||
)
|
||||
from awx.main.utils.db import get_all_field_names
|
||||
from awx.main.views import ApiErrorView
|
||||
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer
|
||||
from awx.api.versioning import URLPathVersioning
|
||||
from awx.api.metadata import SublistAttachDetatchMetadata, Metadata
|
||||
@@ -159,11 +160,11 @@ class APIView(views.APIView):
|
||||
self.queries_before = len(connection.queries)
|
||||
|
||||
# If there are any custom headers in REMOTE_HOST_HEADERS, make sure
|
||||
# they respect the proxy whitelist
|
||||
# they respect the allowed proxy list
|
||||
if all([
|
||||
settings.PROXY_IP_WHITELIST,
|
||||
request.environ.get('REMOTE_ADDR') not in settings.PROXY_IP_WHITELIST,
|
||||
request.environ.get('REMOTE_HOST') not in settings.PROXY_IP_WHITELIST
|
||||
settings.PROXY_IP_ALLOWED_LIST,
|
||||
request.environ.get('REMOTE_ADDR') not in settings.PROXY_IP_ALLOWED_LIST,
|
||||
request.environ.get('REMOTE_HOST') not in settings.PROXY_IP_ALLOWED_LIST
|
||||
]):
|
||||
for custom_header in settings.REMOTE_HOST_HEADERS:
|
||||
if custom_header.startswith('HTTP_'):
|
||||
@@ -188,6 +189,29 @@ class APIView(views.APIView):
|
||||
'''
|
||||
Log warning for 400 requests. Add header with elapsed time.
|
||||
'''
|
||||
|
||||
#
|
||||
# If the URL was rewritten, and we get a 404, we should entirely
|
||||
# replace the view in the request context with an ApiErrorView()
|
||||
# Without this change, there will be subtle differences in the BrowseableAPIRenderer
|
||||
#
|
||||
# These differences could provide contextual clues which would allow
|
||||
# anonymous users to determine if usernames were valid or not
|
||||
# (e.g., if an anonymous user visited `/api/v2/users/valid/`, and got a 404,
|
||||
# but also saw that the page heading said "User Detail", they might notice
|
||||
# that's a difference in behavior from a request to `/api/v2/users/not-valid/`, which
|
||||
# would show a page header of "Not Found"). Changing the view here
|
||||
# guarantees that the rendered response will look exactly like the response
|
||||
# when you visit a URL that has no matching URL paths in `awx.api.urls`.
|
||||
#
|
||||
if response.status_code == 404 and 'awx.named_url_rewritten' in request.environ:
|
||||
self.headers.pop('Allow', None)
|
||||
response = super(APIView, self).finalize_response(request, response, *args, **kwargs)
|
||||
view = ApiErrorView()
|
||||
setattr(view, 'request', request)
|
||||
response.renderer_context['view'] = view
|
||||
return response
|
||||
|
||||
if response.status_code >= 400:
|
||||
status_msg = "status %s received by user %s attempting to access %s from %s" % \
|
||||
(response.status_code, request.user, request.path, request.META.get('REMOTE_ADDR', None))
|
||||
@@ -837,7 +861,7 @@ class CopyAPIView(GenericAPIView):
|
||||
|
||||
@staticmethod
|
||||
def _decrypt_model_field_if_needed(obj, field_name, field_val):
|
||||
if field_name in getattr(type(obj), 'REENCRYPTION_BLACKLIST_AT_COPY', []):
|
||||
if field_name in getattr(type(obj), 'REENCRYPTION_BLOCKLIST_AT_COPY', []):
|
||||
return field_val
|
||||
if isinstance(obj, Credential) and field_name == 'inputs':
|
||||
for secret in obj.credential_type.secret_fields:
|
||||
@@ -883,7 +907,7 @@ class CopyAPIView(GenericAPIView):
|
||||
field_val = getattr(obj, field.name)
|
||||
except AttributeError:
|
||||
continue
|
||||
# Adjust copy blacklist fields here.
|
||||
# Adjust copy blocked fields here.
|
||||
if field.name in fields_to_discard or field.name in [
|
||||
'id', 'pk', 'polymorphic_ctype', 'unifiedjobtemplate_ptr', 'created_by', 'modified_by'
|
||||
] or field.name.endswith('_role'):
|
||||
|
||||
@@ -1697,6 +1697,7 @@ class HostSerializer(BaseSerializerWithVariables):
|
||||
d.setdefault('recent_jobs', [{
|
||||
'id': j.job.id,
|
||||
'name': j.job.job_template.name if j.job.job_template is not None else "",
|
||||
'type': j.job.job_type_name,
|
||||
'status': j.job.status,
|
||||
'finished': j.job.finished,
|
||||
} for j in obj.job_host_summaries.select_related('job__job_template').order_by('-created')[:5]])
|
||||
@@ -1731,6 +1732,7 @@ class HostSerializer(BaseSerializerWithVariables):
|
||||
|
||||
def validate(self, attrs):
|
||||
name = force_text(attrs.get('name', self.instance and self.instance.name or ''))
|
||||
inventory = attrs.get('inventory', self.instance and self.instance.inventory or '')
|
||||
host, port = self._get_host_port_from_name(name)
|
||||
|
||||
if port:
|
||||
@@ -1739,7 +1741,9 @@ class HostSerializer(BaseSerializerWithVariables):
|
||||
vars_dict = parse_yaml_or_json(variables)
|
||||
vars_dict['ansible_ssh_port'] = port
|
||||
attrs['variables'] = json.dumps(vars_dict)
|
||||
|
||||
if Group.objects.filter(name=name, inventory=inventory).exists():
|
||||
raise serializers.ValidationError(_('A Group with that name already exists.'))
|
||||
|
||||
return super(HostSerializer, self).validate(attrs)
|
||||
|
||||
def to_representation(self, obj):
|
||||
@@ -1805,6 +1809,13 @@ class GroupSerializer(BaseSerializerWithVariables):
|
||||
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
||||
return res
|
||||
|
||||
def validate(self, attrs):
|
||||
name = force_text(attrs.get('name', self.instance and self.instance.name or ''))
|
||||
inventory = attrs.get('inventory', self.instance and self.instance.inventory or '')
|
||||
if Host.objects.filter(name=name, inventory=inventory).exists():
|
||||
raise serializers.ValidationError(_('A Host with that name already exists.'))
|
||||
return super(GroupSerializer, self).validate(attrs)
|
||||
|
||||
def validate_name(self, value):
|
||||
if value in ('all', '_meta'):
|
||||
raise serializers.ValidationError(_('Invalid group name.'))
|
||||
@@ -1936,7 +1947,7 @@ class InventorySourceOptionsSerializer(BaseSerializer):
|
||||
def validate_source_vars(self, value):
|
||||
ret = vars_validate_or_raise(value)
|
||||
for env_k in parse_yaml_or_json(value):
|
||||
if env_k in settings.INV_ENV_VARIABLE_BLACKLIST:
|
||||
if env_k in settings.INV_ENV_VARIABLE_BLOCKED:
|
||||
raise serializers.ValidationError(_("`{}` is a prohibited environment variable".format(env_k)))
|
||||
return ret
|
||||
|
||||
@@ -2831,7 +2842,7 @@ class JobTemplateMixin(object):
|
||||
return [{
|
||||
'id': x.id, 'status': x.status, 'finished': x.finished, 'canceled_on': x.canceled_on,
|
||||
# Make type consistent with API top-level key, for instance workflow_job
|
||||
'type': x.get_real_instance_class()._meta.verbose_name.replace(' ', '_')
|
||||
'type': x.job_type_name
|
||||
} for x in optimized_qs[:10]]
|
||||
|
||||
def get_summary_fields(self, obj):
|
||||
@@ -4089,7 +4100,8 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
errors.setdefault('credentials', []).append(_(
|
||||
'Cannot assign multiple {} credentials.'
|
||||
).format(cred.unique_hash(display=True)))
|
||||
if cred.credential_type.kind not in ('ssh', 'vault', 'cloud', 'net'):
|
||||
if cred.credential_type.kind not in ('ssh', 'vault', 'cloud',
|
||||
'net', 'kubernetes'):
|
||||
errors.setdefault('credentials', []).append(_(
|
||||
'Cannot assign a Credential of kind `{}`'
|
||||
).format(cred.credential_type.kind))
|
||||
@@ -4653,6 +4665,8 @@ class InstanceSerializer(BaseSerializer):
|
||||
|
||||
class InstanceGroupSerializer(BaseSerializer):
|
||||
|
||||
show_capabilities = ['edit', 'delete']
|
||||
|
||||
committed_capacity = serializers.SerializerMethodField()
|
||||
consumed_capacity = serializers.SerializerMethodField()
|
||||
percent_capacity_remaining = serializers.SerializerMethodField()
|
||||
|
||||
@@ -14,6 +14,8 @@ import time
|
||||
from base64 import b64encode
|
||||
from collections import OrderedDict
|
||||
|
||||
from urllib3.exceptions import ConnectTimeoutError
|
||||
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
@@ -171,6 +173,15 @@ def api_exception_handler(exc, context):
|
||||
exc = ParseError(exc.args[0])
|
||||
if isinstance(context['view'], UnifiedJobStdout):
|
||||
context['view'].renderer_classes = [renderers.BrowsableAPIRenderer, JSONRenderer]
|
||||
if isinstance(exc, APIException):
|
||||
req = context['request']._request
|
||||
if 'awx.named_url_rewritten' in req.environ and not str(getattr(exc, 'status_code', 0)).startswith('2'):
|
||||
# if the URL was rewritten, and it's not a 2xx level status code,
|
||||
# revert the request.path to its original value to avoid leaking
|
||||
# any context about the existance of resources
|
||||
req.path = req.environ['awx.named_url_rewritten']
|
||||
if exc.status_code == 403:
|
||||
exc = NotFound(detail=_('Not found.'))
|
||||
return exception_handler(exc, context)
|
||||
|
||||
|
||||
@@ -1397,10 +1408,18 @@ class CredentialExternalTest(SubDetailAPIView):
|
||||
obj.credential_type.plugin.backend(**backend_kwargs)
|
||||
return Response({}, status=status.HTTP_202_ACCEPTED)
|
||||
except requests.exceptions.HTTPError as exc:
|
||||
message = 'HTTP {}\n{}'.format(exc.response.status_code, exc.response.text)
|
||||
message = 'HTTP {}'.format(exc.response.status_code)
|
||||
return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Exception as exc:
|
||||
return Response({'inputs': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
message = exc.__class__.__name__
|
||||
args = getattr(exc, 'args', [])
|
||||
for a in args:
|
||||
if isinstance(
|
||||
getattr(a, 'reason', None),
|
||||
ConnectTimeoutError
|
||||
):
|
||||
message = str(a.reason)
|
||||
return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView):
|
||||
@@ -1449,10 +1468,18 @@ class CredentialTypeExternalTest(SubDetailAPIView):
|
||||
obj.plugin.backend(**backend_kwargs)
|
||||
return Response({}, status=status.HTTP_202_ACCEPTED)
|
||||
except requests.exceptions.HTTPError as exc:
|
||||
message = 'HTTP {}\n{}'.format(exc.response.status_code, exc.response.text)
|
||||
message = 'HTTP {}'.format(exc.response.status_code)
|
||||
return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Exception as exc:
|
||||
return Response({'inputs': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
message = exc.__class__.__name__
|
||||
args = getattr(exc, 'args', [])
|
||||
for a in args:
|
||||
if isinstance(
|
||||
getattr(a, 'reason', None),
|
||||
ConnectTimeoutError
|
||||
):
|
||||
message = str(a.reason)
|
||||
return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class HostRelatedSearchMixin(object):
|
||||
@@ -2657,7 +2684,7 @@ class JobTemplateCredentialsList(SubListCreateAttachDetachAPIView):
|
||||
return {"error": _("Cannot assign multiple {credential_type} credentials.").format(
|
||||
credential_type=sub.unique_hash(display=True))}
|
||||
kind = sub.credential_type.kind
|
||||
if kind not in ('ssh', 'vault', 'cloud', 'net'):
|
||||
if kind not in ('ssh', 'vault', 'cloud', 'net', 'kubernetes'):
|
||||
return {'error': _('Cannot assign a Credential of kind `{}`.').format(kind)}
|
||||
|
||||
return super(JobTemplateCredentialsList, self).is_valid_relation(parent, sub, created)
|
||||
|
||||
19
awx/conf/migrations/0007_v380_rename_more_settings.py
Normal file
19
awx/conf/migrations/0007_v380_rename_more_settings.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
from django.db import migrations
|
||||
from awx.conf.migrations import _rename_setting
|
||||
|
||||
|
||||
def copy_allowed_ips(apps, schema_editor):
|
||||
_rename_setting.rename_setting(apps, schema_editor, old_key='PROXY_IP_WHITELIST', new_key='PROXY_IP_ALLOWED_LIST')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('conf', '0006_v331_ldap_group_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(copy_allowed_ips),
|
||||
]
|
||||
@@ -1513,8 +1513,7 @@ class JobTemplateAccess(NotificationAttachMixin, BaseAccess):
|
||||
thus can be made by a job template administrator which may not have access
|
||||
to the any inventory, project, or credentials associated with the template.
|
||||
'''
|
||||
# We are white listing fields that can
|
||||
field_whitelist = [
|
||||
allowed_fields = [
|
||||
'name', 'description', 'forks', 'limit', 'verbosity', 'extra_vars',
|
||||
'job_tags', 'force_handlers', 'skip_tags', 'ask_variables_on_launch',
|
||||
'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_skip_tags_on_launch',
|
||||
@@ -1529,7 +1528,7 @@ class JobTemplateAccess(NotificationAttachMixin, BaseAccess):
|
||||
if k not in [x.name for x in obj._meta.concrete_fields]:
|
||||
continue
|
||||
if hasattr(obj, k) and getattr(obj, k) != v:
|
||||
if k not in field_whitelist and v != getattr(obj, '%s_id' % k, None) \
|
||||
if k not in allowed_fields and v != getattr(obj, '%s_id' % k, None) \
|
||||
and not (hasattr(obj, '%s_id' % k) and getattr(obj, '%s_id' % k) is None and v == ''): # Equate '' to None in the case of foreign keys
|
||||
return False
|
||||
return True
|
||||
@@ -2480,13 +2479,16 @@ class NotificationAccess(BaseAccess):
|
||||
|
||||
class LabelAccess(BaseAccess):
|
||||
'''
|
||||
I can see/use a Label if I have permission to associated organization
|
||||
I can see/use a Label if I have permission to associated organization, or to a JT that the label is on
|
||||
'''
|
||||
model = Label
|
||||
prefetch_related = ('modified_by', 'created_by', 'organization',)
|
||||
|
||||
def filtered_queryset(self):
|
||||
return self.model.objects.all()
|
||||
return self.model.objects.filter(
|
||||
Q(organization__in=Organization.accessible_pk_qs(self.user, 'read_role')) |
|
||||
Q(unifiedjobtemplate_labels__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role'))
|
||||
)
|
||||
|
||||
@check_superuser
|
||||
def can_add(self, data):
|
||||
|
||||
@@ -80,11 +80,11 @@ register(
|
||||
)
|
||||
|
||||
register(
|
||||
'PROXY_IP_WHITELIST',
|
||||
'PROXY_IP_ALLOWED_LIST',
|
||||
field_class=fields.StringListField,
|
||||
label=_('Proxy IP Whitelist'),
|
||||
label=_('Proxy IP Allowed List'),
|
||||
help_text=_("If Tower is behind a reverse proxy/load balancer, use this setting "
|
||||
"to whitelist the proxy IP addresses from which Tower should trust "
|
||||
"to configure the proxy IP addresses from which Tower should trust "
|
||||
"custom REMOTE_HOST_HEADERS header values. "
|
||||
"If this setting is an empty list (the default), the headers specified by "
|
||||
"REMOTE_HOST_HEADERS will be trusted unconditionally')"),
|
||||
@@ -241,7 +241,7 @@ register(
|
||||
field_class=fields.StringListField,
|
||||
required=False,
|
||||
label=_('Paths to expose to isolated jobs'),
|
||||
help_text=_('Whitelist of paths that would otherwise be hidden to expose to isolated jobs. Enter one path per line.'),
|
||||
help_text=_('List of paths that would otherwise be hidden to expose to isolated jobs. Enter one path per line.'),
|
||||
category=_('Jobs'),
|
||||
category_slug='jobs',
|
||||
)
|
||||
@@ -458,7 +458,8 @@ register(
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
label=_('Primary Galaxy Server Username'),
|
||||
help_text=_('For using a galaxy server at higher precedence than the public Ansible Galaxy. '
|
||||
help_text=_('(This setting is deprecated and will be removed in a future release) '
|
||||
'For using a galaxy server at higher precedence than the public Ansible Galaxy. '
|
||||
'The username to use for basic authentication against the Galaxy instance, '
|
||||
'this is mutually exclusive with PRIMARY_GALAXY_TOKEN.'),
|
||||
category=_('Jobs'),
|
||||
@@ -472,7 +473,8 @@ register(
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
label=_('Primary Galaxy Server Password'),
|
||||
help_text=_('For using a galaxy server at higher precedence than the public Ansible Galaxy. '
|
||||
help_text=_('(This setting is deprecated and will be removed in a future release) '
|
||||
'For using a galaxy server at higher precedence than the public Ansible Galaxy. '
|
||||
'The password to use for basic authentication against the Galaxy instance, '
|
||||
'this is mutually exclusive with PRIMARY_GALAXY_TOKEN.'),
|
||||
category=_('Jobs'),
|
||||
|
||||
@@ -31,7 +31,7 @@ STANDARD_INVENTORY_UPDATE_ENV = {
|
||||
CAN_CANCEL = ('new', 'pending', 'waiting', 'running')
|
||||
ACTIVE_STATES = CAN_CANCEL
|
||||
CENSOR_VALUE = '************'
|
||||
ENV_BLACKLIST = frozenset((
|
||||
ENV_BLOCKLIST = frozenset((
|
||||
'VIRTUAL_ENV', 'PATH', 'PYTHONPATH', 'PROOT_TMP_DIR', 'JOB_ID',
|
||||
'INVENTORY_ID', 'INVENTORY_SOURCE_ID', 'INVENTORY_UPDATE_ID',
|
||||
'AD_HOC_COMMAND_ID', 'REST_API_URL', 'REST_API_TOKEN', 'MAX_EVENT_RES',
|
||||
@@ -41,7 +41,7 @@ ENV_BLACKLIST = frozenset((
|
||||
))
|
||||
|
||||
# loggers that may be called in process of emitting a log
|
||||
LOGGER_BLACKLIST = (
|
||||
LOGGER_BLOCKLIST = (
|
||||
'awx.main.utils.handlers',
|
||||
'awx.main.utils.formatters',
|
||||
'awx.main.utils.filters',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .plugin import CredentialPlugin, CertFiles
|
||||
from .plugin import CredentialPlugin, CertFiles, raise_for_status
|
||||
|
||||
from urllib.parse import quote, urlencode, urljoin
|
||||
|
||||
@@ -82,8 +82,9 @@ def aim_backend(**kwargs):
|
||||
timeout=30,
|
||||
cert=cert,
|
||||
verify=verify,
|
||||
allow_redirects=False,
|
||||
)
|
||||
res.raise_for_status()
|
||||
raise_for_status(res)
|
||||
return res.json()['Content']
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .plugin import CredentialPlugin, CertFiles
|
||||
from .plugin import CredentialPlugin, CertFiles, raise_for_status
|
||||
|
||||
import base64
|
||||
from urllib.parse import urljoin, quote
|
||||
@@ -58,7 +58,8 @@ def conjur_backend(**kwargs):
|
||||
|
||||
auth_kwargs = {
|
||||
'headers': {'Content-Type': 'text/plain'},
|
||||
'data': api_key
|
||||
'data': api_key,
|
||||
'allow_redirects': False,
|
||||
}
|
||||
|
||||
with CertFiles(cacert) as cert:
|
||||
@@ -68,11 +69,12 @@ def conjur_backend(**kwargs):
|
||||
urljoin(url, '/'.join(['authn', account, username, 'authenticate'])),
|
||||
**auth_kwargs
|
||||
)
|
||||
resp.raise_for_status()
|
||||
raise_for_status(resp)
|
||||
token = base64.b64encode(resp.content).decode('utf-8')
|
||||
|
||||
lookup_kwargs = {
|
||||
'headers': {'Authorization': 'Token token="{}"'.format(token)},
|
||||
'allow_redirects': False,
|
||||
}
|
||||
|
||||
# https://www.conjur.org/api.html#secrets-retrieve-a-secret-get
|
||||
@@ -88,7 +90,7 @@ def conjur_backend(**kwargs):
|
||||
with CertFiles(cacert) as cert:
|
||||
lookup_kwargs['verify'] = cert
|
||||
resp = requests.get(path, timeout=30, **lookup_kwargs)
|
||||
resp.raise_for_status()
|
||||
raise_for_status(resp)
|
||||
return resp.text
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import pathlib
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from .plugin import CredentialPlugin, CertFiles
|
||||
from .plugin import CredentialPlugin, CertFiles, raise_for_status
|
||||
|
||||
import requests
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@@ -145,7 +145,10 @@ def kv_backend(**kwargs):
|
||||
cacert = kwargs.get('cacert', None)
|
||||
api_version = kwargs['api_version']
|
||||
|
||||
request_kwargs = {'timeout': 30}
|
||||
request_kwargs = {
|
||||
'timeout': 30,
|
||||
'allow_redirects': False,
|
||||
}
|
||||
|
||||
sess = requests.Session()
|
||||
sess.headers['Authorization'] = 'Bearer {}'.format(token)
|
||||
@@ -175,7 +178,7 @@ def kv_backend(**kwargs):
|
||||
with CertFiles(cacert) as cert:
|
||||
request_kwargs['verify'] = cert
|
||||
response = sess.get(request_url, **request_kwargs)
|
||||
response.raise_for_status()
|
||||
raise_for_status(response)
|
||||
|
||||
json = response.json()
|
||||
if api_version == 'v2':
|
||||
@@ -198,7 +201,10 @@ def ssh_backend(**kwargs):
|
||||
role = kwargs['role']
|
||||
cacert = kwargs.get('cacert', None)
|
||||
|
||||
request_kwargs = {'timeout': 30}
|
||||
request_kwargs = {
|
||||
'timeout': 30,
|
||||
'allow_redirects': False,
|
||||
}
|
||||
|
||||
request_kwargs['json'] = {'public_key': kwargs['public_key']}
|
||||
if kwargs.get('valid_principals'):
|
||||
@@ -215,7 +221,7 @@ def ssh_backend(**kwargs):
|
||||
request_kwargs['verify'] = cert
|
||||
resp = sess.post(request_url, **request_kwargs)
|
||||
|
||||
resp.raise_for_status()
|
||||
raise_for_status(resp)
|
||||
return resp.json()['data']['signed_key']
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,19 @@ import tempfile
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
CredentialPlugin = namedtuple('CredentialPlugin', ['name', 'inputs', 'backend'])
|
||||
|
||||
|
||||
def raise_for_status(resp):
|
||||
resp.raise_for_status()
|
||||
if resp.status_code >= 300:
|
||||
exc = HTTPError()
|
||||
setattr(exc, 'response', resp)
|
||||
raise exc
|
||||
|
||||
|
||||
class CertFiles():
|
||||
"""
|
||||
A context manager used for writing a certificate and (optional) key
|
||||
|
||||
@@ -43,7 +43,7 @@ class Control(object):
|
||||
for reply in conn.events(select_timeout=timeout, yield_timeouts=True):
|
||||
if reply is None:
|
||||
logger.error(f'{self.service} did not reply within {timeout}s')
|
||||
raise RuntimeError("{self.service} did not reply within {timeout}s")
|
||||
raise RuntimeError(f"{self.service} did not reply within {timeout}s")
|
||||
break
|
||||
|
||||
return json.loads(reply.payload)
|
||||
|
||||
@@ -7,8 +7,8 @@ import json
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
from jinja2 import Environment, StrictUndefined
|
||||
from jinja2.exceptions import UndefinedError, TemplateSyntaxError
|
||||
from jinja2 import sandbox, StrictUndefined
|
||||
from jinja2.exceptions import UndefinedError, TemplateSyntaxError, SecurityError
|
||||
|
||||
# Django
|
||||
from django.contrib.postgres.fields import JSONField as upstream_JSONBField
|
||||
@@ -50,7 +50,7 @@ from awx.main.models.rbac import (
|
||||
batch_role_ancestor_rebuilding, Role,
|
||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR
|
||||
)
|
||||
from awx.main.constants import ENV_BLACKLIST
|
||||
from awx.main.constants import ENV_BLOCKLIST
|
||||
from awx.main import utils
|
||||
|
||||
|
||||
@@ -637,6 +637,14 @@ class CredentialInputField(JSONSchemaField):
|
||||
else:
|
||||
decrypted_values[k] = v
|
||||
|
||||
# don't allow secrets with $encrypted$ on new object creation
|
||||
if not model_instance.pk:
|
||||
for field in model_instance.credential_type.secret_fields:
|
||||
if value.get(field) == '$encrypted$':
|
||||
raise serializers.ValidationError({
|
||||
self.name: [f'$encrypted$ is a reserved keyword, and cannot be used for {field}.']
|
||||
})
|
||||
|
||||
super(JSONSchemaField, self).validate(decrypted_values, model_instance)
|
||||
errors = {}
|
||||
for error in Draft4Validator(
|
||||
@@ -870,9 +878,9 @@ class CredentialTypeInjectorField(JSONSchemaField):
|
||||
'use is not allowed in credentials.').format(env_var),
|
||||
code='invalid', params={'value': env_var},
|
||||
)
|
||||
if env_var in ENV_BLACKLIST:
|
||||
if env_var in ENV_BLOCKLIST:
|
||||
raise django_exceptions.ValidationError(
|
||||
_('Environment variable {} is blacklisted from use in credentials.').format(env_var),
|
||||
_('Environment variable {} is not allowed to be used in credentials.').format(env_var),
|
||||
code='invalid', params={'value': env_var},
|
||||
)
|
||||
|
||||
@@ -932,7 +940,7 @@ class CredentialTypeInjectorField(JSONSchemaField):
|
||||
self.validate_env_var_allowed(key)
|
||||
for key, tmpl in injector.items():
|
||||
try:
|
||||
Environment(
|
||||
sandbox.ImmutableSandboxedEnvironment(
|
||||
undefined=StrictUndefined
|
||||
).from_string(tmpl).render(valid_namespace)
|
||||
except UndefinedError as e:
|
||||
@@ -942,6 +950,10 @@ class CredentialTypeInjectorField(JSONSchemaField):
|
||||
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(
|
||||
|
||||
96
awx/main/management/commands/bottleneck.py
Normal file
96
awx/main/management/commands/bottleneck.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import connection
|
||||
|
||||
from awx.main.models import JobTemplate
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Find the slowest tasks and hosts for a Job Template's most recent runs."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--template', dest='jt', type=int,
|
||||
help='ID of the Job Template to profile')
|
||||
parser.add_argument('--threshold', dest='threshold', type=float, default=30,
|
||||
help='Only show tasks that took at least this many seconds (defaults to 30)')
|
||||
parser.add_argument('--history', dest='history', type=float, default=25,
|
||||
help='The number of historic jobs to look at')
|
||||
parser.add_argument('--ignore', action='append', help='ignore a specific action (e.g., --ignore git)')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
jt = options['jt']
|
||||
threshold = options['threshold']
|
||||
history = options['history']
|
||||
ignore = options['ignore']
|
||||
|
||||
print('## ' + JobTemplate.objects.get(pk=jt).name + f' (last {history} runs)\n')
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
f'''
|
||||
SELECT
|
||||
b.id, b.job_id, b.host_name, b.created - a.created delta,
|
||||
b.task task,
|
||||
b.event_data::json->'task_action' task_action,
|
||||
b.event_data::json->'task_path' task_path
|
||||
FROM main_jobevent a JOIN main_jobevent b
|
||||
ON b.parent_uuid = a.parent_uuid AND a.host_name = b.host_name
|
||||
WHERE
|
||||
a.event = 'runner_on_start' AND
|
||||
b.event != 'runner_on_start' AND
|
||||
b.event != 'runner_on_skipped' AND
|
||||
b.failed = false AND
|
||||
a.job_id IN (
|
||||
SELECT unifiedjob_ptr_id FROM main_job
|
||||
WHERE job_template_id={jt}
|
||||
ORDER BY unifiedjob_ptr_id DESC
|
||||
LIMIT {history}
|
||||
)
|
||||
ORDER BY delta DESC;
|
||||
'''
|
||||
)
|
||||
slowest_events = cursor.fetchall()
|
||||
|
||||
def format_td(x):
|
||||
return str(x).split('.')[0]
|
||||
|
||||
fastest = dict()
|
||||
for event in slowest_events:
|
||||
_id, job_id, host, duration, task, action, playbook = event
|
||||
playbook = playbook.rsplit('/')[-1]
|
||||
if ignore and action in ignore:
|
||||
continue
|
||||
if host:
|
||||
fastest[(action, playbook)] = (_id, host, format_td(duration))
|
||||
|
||||
host_counts = dict()
|
||||
warned = set()
|
||||
print(f'slowest tasks (--threshold={threshold})\n---')
|
||||
|
||||
for event in slowest_events:
|
||||
_id, job_id, host, duration, task, action, playbook = event
|
||||
if ignore and action in ignore:
|
||||
continue
|
||||
if duration.total_seconds() < threshold:
|
||||
break
|
||||
playbook = playbook.rsplit('/')[-1]
|
||||
human_duration = format_td(duration)
|
||||
|
||||
fastest_summary = ''
|
||||
fastest_match = fastest.get((action, playbook))
|
||||
if fastest_match[2] != human_duration and (host, action, playbook) not in warned:
|
||||
warned.add((host, action, playbook))
|
||||
fastest_summary = ' ' + self.style.WARNING(f'{fastest_match[1]} ran this in {fastest_match[2]}s at /api/v2/job_events/{fastest_match[0]}/')
|
||||
|
||||
url = f'/api/v2/jobs/{job_id}/'
|
||||
print(' -- '.join([url, host, human_duration, action, task, playbook]) + fastest_summary)
|
||||
host_counts.setdefault(host, [])
|
||||
host_counts[host].append(duration)
|
||||
|
||||
host_counts = sorted(host_counts.items(), key=lambda item: [e.total_seconds() for e in item[1]], reverse=True)
|
||||
|
||||
print('\nslowest hosts\n---')
|
||||
for h, matches in host_counts:
|
||||
total = len(matches)
|
||||
total_seconds = sum([e.total_seconds() for e in matches])
|
||||
print(f'{h} had {total} tasks that ran longer than {threshold} second(s) for a total of {total_seconds}')
|
||||
|
||||
print('')
|
||||
@@ -19,3 +19,7 @@ class Command(BaseCommand):
|
||||
profile_sql.delay(
|
||||
threshold=options['threshold'], minutes=options['minutes']
|
||||
)
|
||||
print(f"Logging initiated with a threshold of {options['threshold']} second(s) and a duration of"
|
||||
f" {options['minutes']} minute(s), any queries that meet criteria can"
|
||||
f" be found in /var/log/tower/profile/."
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.migrations.executor import MigrationExecutor
|
||||
from django.db import connection
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.shortcuts import redirect
|
||||
from django.apps import apps
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@@ -148,7 +148,21 @@ class URLModificationMiddleware(MiddlewareMixin):
|
||||
def _named_url_to_pk(cls, node, resource, named_url):
|
||||
kwargs = {}
|
||||
if node.populate_named_url_query_kwargs(kwargs, named_url):
|
||||
return str(get_object_or_404(node.model, **kwargs).pk)
|
||||
match = node.model.objects.filter(**kwargs).first()
|
||||
if match:
|
||||
return str(match.pk)
|
||||
else:
|
||||
# if the name does *not* resolve to any actual resource,
|
||||
# we should still attempt to route it through so that 401s are
|
||||
# respected
|
||||
# using "zero" here will cause the URL regex to match e.g.,
|
||||
# /api/v2/users/<integer>/, but it also means that anonymous
|
||||
# users will go down the path of having their credentials
|
||||
# verified; in this way, *anonymous* users will that visit
|
||||
# /api/v2/users/invalid-username/ *won't* see a 404, they'll
|
||||
# see a 401 as if they'd gone to /api/v2/users/0/
|
||||
#
|
||||
return '0'
|
||||
if resource == 'job_templates' and '++' not in named_url:
|
||||
# special case for deprecated job template case
|
||||
# will not raise a 404 on its own
|
||||
@@ -178,6 +192,7 @@ class URLModificationMiddleware(MiddlewareMixin):
|
||||
old_path = request.path_info
|
||||
new_path = self._convert_named_url(old_path)
|
||||
if request.path_info != new_path:
|
||||
request.environ['awx.named_url_rewritten'] = request.path
|
||||
request.path = request.path.replace(request.path_info, new_path)
|
||||
request.path_info = new_path
|
||||
|
||||
|
||||
@@ -127,9 +127,15 @@ def user_get_auditor_of_organizations(user):
|
||||
return Organization.objects.filter(auditor_role__members=user)
|
||||
|
||||
|
||||
@property
|
||||
def created(user):
|
||||
return user.date_joined
|
||||
|
||||
|
||||
User.add_to_class('organizations', user_get_organizations)
|
||||
User.add_to_class('admin_of_organizations', user_get_admin_of_organizations)
|
||||
User.add_to_class('auditor_of_organizations', user_get_auditor_of_organizations)
|
||||
User.add_to_class('created', created)
|
||||
|
||||
|
||||
@property
|
||||
|
||||
@@ -407,7 +407,7 @@ def prevent_search(relation):
|
||||
sensitive_data = prevent_search(models.CharField(...))
|
||||
|
||||
The flag set by this function is used by
|
||||
`awx.api.filters.FieldLookupBackend` to blacklist fields and relations that
|
||||
`awx.api.filters.FieldLookupBackend` to block fields and relations that
|
||||
should not be searchable/filterable via search query params
|
||||
"""
|
||||
setattr(relation, '__prevent_search__', True)
|
||||
|
||||
@@ -11,7 +11,7 @@ import tempfile
|
||||
from types import SimpleNamespace
|
||||
|
||||
# Jinja2
|
||||
from jinja2 import Template
|
||||
from jinja2 import sandbox
|
||||
|
||||
# Django
|
||||
from django.db import models
|
||||
@@ -514,8 +514,11 @@ class CredentialType(CommonModelNameNotUnique):
|
||||
# If any file templates are provided, render the files and update the
|
||||
# special `tower` template namespace so the filename can be
|
||||
# referenced in other injectors
|
||||
|
||||
sandbox_env = sandbox.ImmutableSandboxedEnvironment()
|
||||
|
||||
for file_label, file_tmpl in file_tmpls.items():
|
||||
data = Template(file_tmpl).render(**namespace)
|
||||
data = sandbox_env.from_string(file_tmpl).render(**namespace)
|
||||
_, path = tempfile.mkstemp(dir=private_data_dir)
|
||||
with open(path, 'w') as f:
|
||||
f.write(data)
|
||||
@@ -537,14 +540,14 @@ class CredentialType(CommonModelNameNotUnique):
|
||||
except ValidationError as e:
|
||||
logger.error('Ignoring prohibited env var {}, reason: {}'.format(env_var, e))
|
||||
continue
|
||||
env[env_var] = Template(tmpl).render(**namespace)
|
||||
safe_env[env_var] = Template(tmpl).render(**safe_namespace)
|
||||
env[env_var] = sandbox_env.from_string(tmpl).render(**namespace)
|
||||
safe_env[env_var] = sandbox_env.from_string(tmpl).render(**safe_namespace)
|
||||
|
||||
if 'INVENTORY_UPDATE_ID' not in env:
|
||||
# awx-manage inventory_update does not support extra_vars via -e
|
||||
extra_vars = {}
|
||||
for var_name, tmpl in self.injectors.get('extra_vars', {}).items():
|
||||
extra_vars[var_name] = Template(tmpl).render(**namespace)
|
||||
extra_vars[var_name] = sandbox_env.from_string(tmpl).render(**namespace)
|
||||
|
||||
def build_extra_vars_file(vars, private_dir):
|
||||
handle, path = tempfile.mkstemp(dir = private_dir)
|
||||
@@ -1103,26 +1106,36 @@ ManagedCredentialType(
|
||||
}, {
|
||||
'id': 'username',
|
||||
'label': ugettext_noop('Username'),
|
||||
'type': 'string'
|
||||
'type': 'string',
|
||||
'help_text': ugettext_noop('The Ansible Tower user to authenticate as.'
|
||||
'This should not be set if an OAuth token is being used.')
|
||||
}, {
|
||||
'id': 'password',
|
||||
'label': ugettext_noop('Password'),
|
||||
'type': 'string',
|
||||
'secret': True,
|
||||
}, {
|
||||
'id': 'oauth_token',
|
||||
'label': ugettext_noop('OAuth Token'),
|
||||
'type': 'string',
|
||||
'secret': True,
|
||||
'help_text': ugettext_noop('An OAuth token to use to authenticate to Tower with.'
|
||||
'This should not be set if username/password are being used.')
|
||||
}, {
|
||||
'id': 'verify_ssl',
|
||||
'label': ugettext_noop('Verify SSL'),
|
||||
'type': 'boolean',
|
||||
'secret': False
|
||||
}],
|
||||
'required': ['host', 'username', 'password'],
|
||||
'required': ['host'],
|
||||
},
|
||||
injectors={
|
||||
'env': {
|
||||
'TOWER_HOST': '{{host}}',
|
||||
'TOWER_USERNAME': '{{username}}',
|
||||
'TOWER_PASSWORD': '{{password}}',
|
||||
'TOWER_VERIFY_SSL': '{{verify_ssl}}'
|
||||
'TOWER_VERIFY_SSL': '{{verify_ssl}}',
|
||||
'TOWER_OAUTH_TOKEN': '{{oauth_token}}'
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -101,3 +101,17 @@ def openstack(cred, env, private_data_dir):
|
||||
f.close()
|
||||
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
env['OS_CLIENT_CONFIG_FILE'] = path
|
||||
|
||||
|
||||
def kubernetes_bearer_token(cred, env, private_data_dir):
|
||||
env['K8S_AUTH_HOST'] = cred.get_input('host', default='')
|
||||
env['K8S_AUTH_API_KEY'] = cred.get_input('bearer_token', default='')
|
||||
if cred.get_input('verify_ssl') and 'ssl_ca_cert' in cred.inputs:
|
||||
env['K8S_AUTH_VERIFY_SSL'] = 'True'
|
||||
handle, path = tempfile.mkstemp(dir=private_data_dir)
|
||||
with os.fdopen(handle, 'w') as f:
|
||||
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
f.write(cred.get_input('ssl_ca_cert'))
|
||||
env['K8S_AUTH_SSL_CA_CERT'] = path
|
||||
else:
|
||||
env['K8S_AUTH_VERIFY_SSL'] = 'False'
|
||||
|
||||
@@ -1910,7 +1910,7 @@ class ec2(PluginFileInjector):
|
||||
# Compatibility content
|
||||
legacy_regex = {
|
||||
True: r"[^A-Za-z0-9\_]",
|
||||
False: r"[^A-Za-z0-9\_\-]" # do not replace dash, dash is whitelisted
|
||||
False: r"[^A-Za-z0-9\_\-]" # do not replace dash, dash is allowed
|
||||
}[replace_dash]
|
||||
list_replacer = 'map("regex_replace", "{rx}", "_") | list'.format(rx=legacy_regex)
|
||||
# this option, a plugin option, will allow dashes, but not unicode
|
||||
@@ -1943,7 +1943,7 @@ class ec2(PluginFileInjector):
|
||||
ret['boto_profile'] = source_vars['boto_profile']
|
||||
|
||||
elif not replace_dash:
|
||||
# Using the plugin, but still want dashes whitelisted
|
||||
# Using the plugin, but still want dashes allowed
|
||||
ret['use_contrib_script_compatible_sanitization'] = True
|
||||
|
||||
if source_vars.get('nested_groups') is False:
|
||||
|
||||
@@ -262,25 +262,25 @@ class JobNotificationMixin(object):
|
||||
'running': 'started',
|
||||
'failed': 'error'}
|
||||
# Tree of fields that can be safely referenced in a notification message
|
||||
JOB_FIELDS_WHITELIST = ['id', 'type', 'url', 'created', 'modified', 'name', 'description', 'job_type', 'playbook',
|
||||
'forks', 'limit', 'verbosity', 'job_tags', 'force_handlers', 'skip_tags', 'start_at_task',
|
||||
'timeout', 'use_fact_cache', 'launch_type', 'status', 'failed', 'started', 'finished',
|
||||
'elapsed', 'job_explanation', 'execution_node', 'controller_node', 'allow_simultaneous',
|
||||
'scm_revision', 'diff_mode', 'job_slice_number', 'job_slice_count', 'custom_virtualenv',
|
||||
'approval_status', 'approval_node_name', 'workflow_url', 'scm_branch',
|
||||
{'host_status_counts': ['skipped', 'ok', 'changed', 'failed', 'failures', 'dark'
|
||||
'processed', 'rescued', 'ignored']},
|
||||
{'summary_fields': [{'inventory': ['id', 'name', 'description', 'has_active_failures',
|
||||
'total_hosts', 'hosts_with_active_failures', 'total_groups',
|
||||
'has_inventory_sources',
|
||||
'total_inventory_sources', 'inventory_sources_with_failures',
|
||||
'organization_id', 'kind']},
|
||||
{'project': ['id', 'name', 'description', 'status', 'scm_type']},
|
||||
{'job_template': ['id', 'name', 'description']},
|
||||
{'unified_job_template': ['id', 'name', 'description', 'unified_job_type']},
|
||||
{'instance_group': ['name', 'id']},
|
||||
{'created_by': ['id', 'username', 'first_name', 'last_name']},
|
||||
{'labels': ['count', 'results']}]}]
|
||||
JOB_FIELDS_ALLOWED_LIST = ['id', 'type', 'url', 'created', 'modified', 'name', 'description', 'job_type', 'playbook',
|
||||
'forks', 'limit', 'verbosity', 'job_tags', 'force_handlers', 'skip_tags', 'start_at_task',
|
||||
'timeout', 'use_fact_cache', 'launch_type', 'status', 'failed', 'started', 'finished',
|
||||
'elapsed', 'job_explanation', 'execution_node', 'controller_node', 'allow_simultaneous',
|
||||
'scm_revision', 'diff_mode', 'job_slice_number', 'job_slice_count', 'custom_virtualenv',
|
||||
'approval_status', 'approval_node_name', 'workflow_url', 'scm_branch', 'artifacts',
|
||||
{'host_status_counts': ['skipped', 'ok', 'changed', 'failed', 'failures', 'dark'
|
||||
'processed', 'rescued', 'ignored']},
|
||||
{'summary_fields': [{'inventory': ['id', 'name', 'description', 'has_active_failures',
|
||||
'total_hosts', 'hosts_with_active_failures', 'total_groups',
|
||||
'has_inventory_sources',
|
||||
'total_inventory_sources', 'inventory_sources_with_failures',
|
||||
'organization_id', 'kind']},
|
||||
{'project': ['id', 'name', 'description', 'status', 'scm_type']},
|
||||
{'job_template': ['id', 'name', 'description']},
|
||||
{'unified_job_template': ['id', 'name', 'description', 'unified_job_type']},
|
||||
{'instance_group': ['name', 'id']},
|
||||
{'created_by': ['id', 'username', 'first_name', 'last_name']},
|
||||
{'labels': ['count', 'results']}]}]
|
||||
|
||||
@classmethod
|
||||
def context_stub(cls):
|
||||
@@ -288,6 +288,7 @@ class JobNotificationMixin(object):
|
||||
Context has the same structure as the context that will actually be used to render
|
||||
a notification message."""
|
||||
context = {'job': {'allow_simultaneous': False,
|
||||
'artifacts': {},
|
||||
'controller_node': 'foo_controller',
|
||||
'created': datetime.datetime(2018, 11, 13, 6, 4, 0, 0, tzinfo=datetime.timezone.utc),
|
||||
'custom_virtualenv': 'my_venv',
|
||||
@@ -377,8 +378,8 @@ class JobNotificationMixin(object):
|
||||
|
||||
def context(self, serialized_job):
|
||||
"""Returns a dictionary that can be used for rendering notification messages.
|
||||
The context will contain whitelisted content retrieved from a serialized job object
|
||||
(see JobNotificationMixin.JOB_FIELDS_WHITELIST), the job's friendly name,
|
||||
The context will contain allowed content retrieved from a serialized job object
|
||||
(see JobNotificationMixin.JOB_FIELDS_ALLOWED_LIST the job's friendly name,
|
||||
and a url to the job run."""
|
||||
job_context = {'host_status_counts': {}}
|
||||
summary = None
|
||||
@@ -395,22 +396,22 @@ class JobNotificationMixin(object):
|
||||
'job_metadata': json.dumps(self.notification_data(), indent=4)
|
||||
}
|
||||
|
||||
def build_context(node, fields, whitelisted_fields):
|
||||
for safe_field in whitelisted_fields:
|
||||
def build_context(node, fields, allowed_fields):
|
||||
for safe_field in allowed_fields:
|
||||
if type(safe_field) is dict:
|
||||
field, whitelist_subnode = safe_field.copy().popitem()
|
||||
field, allowed_subnode = safe_field.copy().popitem()
|
||||
# ensure content present in job serialization
|
||||
if field not in fields:
|
||||
continue
|
||||
subnode = fields[field]
|
||||
node[field] = {}
|
||||
build_context(node[field], subnode, whitelist_subnode)
|
||||
build_context(node[field], subnode, allowed_subnode)
|
||||
else:
|
||||
# ensure content present in job serialization
|
||||
if safe_field not in fields:
|
||||
continue
|
||||
node[safe_field] = fields[safe_field]
|
||||
build_context(context['job'], serialized_job, self.JOB_FIELDS_WHITELIST)
|
||||
build_context(context['job'], serialized_job, self.JOB_FIELDS_ALLOWED_LIST)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
@@ -194,6 +194,11 @@ class ProjectOptions(models.Model):
|
||||
if not check_if_exists or os.path.exists(smart_str(proj_path)):
|
||||
return proj_path
|
||||
|
||||
def get_cache_path(self):
|
||||
local_path = os.path.basename(self.local_path)
|
||||
if local_path:
|
||||
return os.path.join(settings.PROJECTS_ROOT, '.__awx_cache', local_path)
|
||||
|
||||
@property
|
||||
def playbooks(self):
|
||||
results = []
|
||||
@@ -418,6 +423,10 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def cache_id(self):
|
||||
return str(self.last_job_id)
|
||||
|
||||
@property
|
||||
def notification_templates(self):
|
||||
base_notification_templates = NotificationTemplate.objects
|
||||
@@ -455,11 +464,12 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
path_to_delete = self.get_project_path(check_if_exists=False)
|
||||
paths_to_delete = (self.get_project_path(check_if_exists=False), self.get_cache_path())
|
||||
r = super(Project, self).delete(*args, **kwargs)
|
||||
if self.scm_type and path_to_delete: # non-manual, concrete path
|
||||
from awx.main.tasks import delete_project_files
|
||||
delete_project_files.delay(path_to_delete)
|
||||
for path_to_delete in paths_to_delete:
|
||||
if self.scm_type and path_to_delete: # non-manual, concrete path
|
||||
from awx.main.tasks import delete_project_files
|
||||
delete_project_files.delay(path_to_delete)
|
||||
return r
|
||||
|
||||
|
||||
@@ -554,6 +564,19 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin, TaskManage
|
||||
def result_stdout_raw(self):
|
||||
return self._result_stdout_raw(redact_sensitive=True)
|
||||
|
||||
@property
|
||||
def branch_override(self):
|
||||
"""Whether a branch other than the project default is used."""
|
||||
if not self.project:
|
||||
return True
|
||||
return bool(self.scm_branch and self.scm_branch != self.project.scm_branch)
|
||||
|
||||
@property
|
||||
def cache_id(self):
|
||||
if self.branch_override or self.job_type == 'check' or (not self.project):
|
||||
return str(self.id)
|
||||
return self.project.cache_id
|
||||
|
||||
def result_stdout_raw_limited(self, start_line=0, end_line=None, redact_sensitive=True):
|
||||
return self._result_stdout_raw_limited(start_line, end_line, redact_sensitive=redact_sensitive)
|
||||
|
||||
@@ -597,10 +620,7 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin, TaskManage
|
||||
def save(self, *args, **kwargs):
|
||||
added_update_fields = []
|
||||
if not self.job_tags:
|
||||
job_tags = ['update_{}'.format(self.scm_type)]
|
||||
if self.job_type == 'run':
|
||||
job_tags.append('install_roles')
|
||||
job_tags.append('install_collections')
|
||||
job_tags = ['update_{}'.format(self.scm_type), 'install_roles', 'install_collections']
|
||||
self.job_tags = ','.join(job_tags)
|
||||
added_update_fields.append('job_tags')
|
||||
if self.scm_delete_on_update and 'delete' not in self.job_tags and self.job_type == 'check':
|
||||
|
||||
@@ -962,6 +962,10 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
def event_class(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def job_type_name(self):
|
||||
return self.get_real_instance_class()._meta.verbose_name.replace(' ', '_')
|
||||
|
||||
@property
|
||||
def result_stdout_text(self):
|
||||
related = UnifiedJobDeprecatedStdout.objects.get(pk=self.pk)
|
||||
@@ -1221,7 +1225,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
|
||||
def websocket_emit_data(self):
|
||||
''' Return extra data that should be included when submitting data to the browser over the websocket connection '''
|
||||
websocket_data = dict(type=self.get_real_instance_class()._meta.verbose_name.replace(' ', '_'))
|
||||
websocket_data = dict(type=self.job_type_name)
|
||||
if self.spawned_by_workflow:
|
||||
websocket_data.update(dict(workflow_job_id=self.workflow_job_id,
|
||||
workflow_node_id=self.workflow_node_id))
|
||||
@@ -1362,7 +1366,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
running = self.celery_task_id in ControlDispatcher(
|
||||
'dispatcher', self.controller_node or self.execution_node
|
||||
).running(timeout=timeout)
|
||||
except socket.timeout:
|
||||
except (socket.timeout, RuntimeError):
|
||||
logger.error('could not reach dispatcher on {} within {}s'.format(
|
||||
self.execution_node, timeout
|
||||
))
|
||||
|
||||
@@ -139,7 +139,7 @@ class WorkflowJobTemplateNode(WorkflowNodeBase):
|
||||
'always_nodes', 'credentials', 'inventory', 'extra_data', 'survey_passwords',
|
||||
'char_prompts', 'all_parents_must_converge', 'identifier'
|
||||
]
|
||||
REENCRYPTION_BLACKLIST_AT_COPY = ['extra_data', 'survey_passwords']
|
||||
REENCRYPTION_BLOCKLIST_AT_COPY = ['extra_data', 'survey_passwords']
|
||||
|
||||
workflow_job_template = models.ForeignKey(
|
||||
'WorkflowJobTemplate',
|
||||
|
||||
@@ -94,8 +94,8 @@ class GrafanaBackend(AWXBaseEmailBackend, CustomNotificationBase):
|
||||
headers=grafana_headers,
|
||||
verify=(not self.grafana_no_verify_ssl))
|
||||
if r.status_code >= 400:
|
||||
logger.error(smart_text(_("Error sending notification grafana: {}").format(r.text)))
|
||||
logger.error(smart_text(_("Error sending notification grafana: {}").format(r.status_code)))
|
||||
if not self.fail_silently:
|
||||
raise Exception(smart_text(_("Error sending notification grafana: {}").format(r.text)))
|
||||
raise Exception(smart_text(_("Error sending notification grafana: {}").format(r.status_code)))
|
||||
sent_messages += 1
|
||||
return sent_messages
|
||||
|
||||
@@ -46,8 +46,8 @@ class MattermostBackend(AWXBaseEmailBackend, CustomNotificationBase):
|
||||
r = requests.post("{}".format(m.recipients()[0]),
|
||||
json=payload, verify=(not self.mattermost_no_verify_ssl))
|
||||
if r.status_code >= 400:
|
||||
logger.error(smart_text(_("Error sending notification mattermost: {}").format(r.text)))
|
||||
logger.error(smart_text(_("Error sending notification mattermost: {}").format(r.status_code)))
|
||||
if not self.fail_silently:
|
||||
raise Exception(smart_text(_("Error sending notification mattermost: {}").format(r.text)))
|
||||
raise Exception(smart_text(_("Error sending notification mattermost: {}").format(r.status_code)))
|
||||
sent_messages += 1
|
||||
return sent_messages
|
||||
|
||||
@@ -46,9 +46,9 @@ class RocketChatBackend(AWXBaseEmailBackend, CustomNotificationBase):
|
||||
|
||||
if r.status_code >= 400:
|
||||
logger.error(smart_text(
|
||||
_("Error sending notification rocket.chat: {}").format(r.text)))
|
||||
_("Error sending notification rocket.chat: {}").format(r.status_code)))
|
||||
if not self.fail_silently:
|
||||
raise Exception(smart_text(
|
||||
_("Error sending notification rocket.chat: {}").format(r.text)))
|
||||
_("Error sending notification rocket.chat: {}").format(r.status_code)))
|
||||
sent_messages += 1
|
||||
return sent_messages
|
||||
|
||||
@@ -72,8 +72,8 @@ class WebhookBackend(AWXBaseEmailBackend, CustomNotificationBase):
|
||||
headers=self.headers,
|
||||
verify=(not self.disable_ssl_verification))
|
||||
if r.status_code >= 400:
|
||||
logger.error(smart_text(_("Error sending notification webhook: {}").format(r.text)))
|
||||
logger.error(smart_text(_("Error sending notification webhook: {}").format(r.status_code)))
|
||||
if not self.fail_silently:
|
||||
raise Exception(smart_text(_("Error sending notification webhook: {}").format(r.text)))
|
||||
raise Exception(smart_text(_("Error sending notification webhook: {}").format(r.status_code)))
|
||||
sent_messages += 1
|
||||
return sent_messages
|
||||
|
||||
@@ -1802,7 +1802,7 @@ class RunJob(BaseTask):
|
||||
|
||||
# By default, all extra vars disallow Jinja2 template usage for
|
||||
# security reasons; top level key-values defined in JT.extra_vars, however,
|
||||
# are whitelisted as "safe" (because they can only be set by users with
|
||||
# are allowed as "safe" (because they can only be set by users with
|
||||
# higher levels of privilege - those that have the ability create and
|
||||
# edit Job Templates)
|
||||
safe_dict = {}
|
||||
@@ -1865,44 +1865,31 @@ class RunJob(BaseTask):
|
||||
project_path = job.project.get_project_path(check_if_exists=False)
|
||||
job_revision = job.project.scm_revision
|
||||
sync_needs = []
|
||||
all_sync_needs = ['update_{}'.format(job.project.scm_type), 'install_roles', 'install_collections']
|
||||
source_update_tag = 'update_{}'.format(job.project.scm_type)
|
||||
branch_override = bool(job.scm_branch and job.scm_branch != job.project.scm_branch)
|
||||
if not job.project.scm_type:
|
||||
pass # manual projects are not synced, user has responsibility for that
|
||||
elif not os.path.exists(project_path):
|
||||
logger.debug('Performing fresh clone of {} on this instance.'.format(job.project))
|
||||
sync_needs = all_sync_needs
|
||||
elif not job.project.scm_revision:
|
||||
logger.debug('Revision not known for {}, will sync with remote'.format(job.project))
|
||||
sync_needs = all_sync_needs
|
||||
elif job.project.scm_type == 'git':
|
||||
sync_needs.append(source_update_tag)
|
||||
elif job.project.scm_type == 'git' and job.project.scm_revision and (not branch_override):
|
||||
git_repo = git.Repo(project_path)
|
||||
try:
|
||||
desired_revision = job.project.scm_revision
|
||||
if job.scm_branch and job.scm_branch != job.project.scm_branch:
|
||||
desired_revision = job.scm_branch # could be commit or not, but will try as commit
|
||||
current_revision = git_repo.head.commit.hexsha
|
||||
if desired_revision == current_revision:
|
||||
job_revision = desired_revision
|
||||
if job_revision == git_repo.head.commit.hexsha:
|
||||
logger.debug('Skipping project sync for {} because commit is locally available'.format(job.log_format))
|
||||
else:
|
||||
sync_needs = all_sync_needs
|
||||
sync_needs.append(source_update_tag)
|
||||
except (ValueError, BadGitName):
|
||||
logger.debug('Needed commit for {} not in local source tree, will sync with remote'.format(job.log_format))
|
||||
sync_needs = all_sync_needs
|
||||
sync_needs.append(source_update_tag)
|
||||
else:
|
||||
sync_needs = all_sync_needs
|
||||
# Galaxy requirements are not supported for manual projects
|
||||
if not sync_needs and job.project.scm_type:
|
||||
# see if we need a sync because of presence of roles
|
||||
galaxy_req_path = os.path.join(project_path, 'roles', 'requirements.yml')
|
||||
if os.path.exists(galaxy_req_path):
|
||||
logger.debug('Running project sync for {} because of galaxy role requirements.'.format(job.log_format))
|
||||
sync_needs.append('install_roles')
|
||||
logger.debug('Project not available locally, {} will sync with remote'.format(job.log_format))
|
||||
sync_needs.append(source_update_tag)
|
||||
|
||||
galaxy_collections_req_path = os.path.join(project_path, 'collections', 'requirements.yml')
|
||||
if os.path.exists(galaxy_collections_req_path):
|
||||
logger.debug('Running project sync for {} because of galaxy collections requirements.'.format(job.log_format))
|
||||
sync_needs.append('install_collections')
|
||||
has_cache = os.path.exists(os.path.join(job.project.get_cache_path(), job.project.cache_id))
|
||||
# Galaxy requirements are not supported for manual projects
|
||||
if job.project.scm_type and ((not has_cache) or branch_override):
|
||||
sync_needs.extend(['install_roles', 'install_collections'])
|
||||
|
||||
if sync_needs:
|
||||
pu_ig = job.instance_group
|
||||
@@ -1920,7 +1907,7 @@ class RunJob(BaseTask):
|
||||
execution_node=pu_en,
|
||||
celery_task_id=job.celery_task_id
|
||||
)
|
||||
if job.scm_branch and job.scm_branch != job.project.scm_branch:
|
||||
if branch_override:
|
||||
sync_metafields['scm_branch'] = job.scm_branch
|
||||
if 'update_' not in sync_metafields['job_tags']:
|
||||
sync_metafields['scm_revision'] = job_revision
|
||||
@@ -1952,10 +1939,7 @@ class RunJob(BaseTask):
|
||||
if job_revision:
|
||||
job = self.update_model(job.pk, scm_revision=job_revision)
|
||||
# Project update does not copy the folder, so copy here
|
||||
RunProjectUpdate.make_local_copy(
|
||||
project_path, os.path.join(private_data_dir, 'project'),
|
||||
job.project.scm_type, job_revision
|
||||
)
|
||||
RunProjectUpdate.make_local_copy(job.project, private_data_dir, scm_revision=job_revision)
|
||||
|
||||
if job.inventory.kind == 'smart':
|
||||
# cache smart inventory memberships so that the host_filter query is not
|
||||
@@ -1995,10 +1979,7 @@ class RunProjectUpdate(BaseTask):
|
||||
|
||||
@property
|
||||
def proot_show_paths(self):
|
||||
show_paths = [settings.PROJECTS_ROOT]
|
||||
if self.job_private_data_dir:
|
||||
show_paths.append(self.job_private_data_dir)
|
||||
return show_paths
|
||||
return [settings.PROJECTS_ROOT]
|
||||
|
||||
def __init__(self, *args, job_private_data_dir=None, **kwargs):
|
||||
super(RunProjectUpdate, self).__init__(*args, **kwargs)
|
||||
@@ -2032,12 +2013,6 @@ class RunProjectUpdate(BaseTask):
|
||||
credential = project_update.credential
|
||||
if credential.has_input('ssh_key_data'):
|
||||
private_data['credentials'][credential] = credential.get_input('ssh_key_data', default='')
|
||||
|
||||
# Create dir where collections will live for the job run
|
||||
if project_update.job_type != 'check' and getattr(self, 'job_private_data_dir'):
|
||||
for folder_name in ('requirements_collections', 'requirements_roles'):
|
||||
folder_path = os.path.join(self.job_private_data_dir, folder_name)
|
||||
os.mkdir(folder_path, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
|
||||
return private_data
|
||||
|
||||
def build_passwords(self, project_update, runtime_passwords):
|
||||
@@ -2165,8 +2140,7 @@ class RunProjectUpdate(BaseTask):
|
||||
extra_vars.update(extra_vars_new)
|
||||
|
||||
scm_branch = project_update.scm_branch
|
||||
branch_override = bool(scm_branch and project_update.scm_branch != project_update.project.scm_branch)
|
||||
if project_update.job_type == 'run' and (not branch_override):
|
||||
if project_update.job_type == 'run' and (not project_update.branch_override):
|
||||
if project_update.project.scm_revision:
|
||||
scm_branch = project_update.project.scm_revision
|
||||
elif not scm_branch:
|
||||
@@ -2174,7 +2148,9 @@ class RunProjectUpdate(BaseTask):
|
||||
elif not scm_branch:
|
||||
scm_branch = {'hg': 'tip'}.get(project_update.scm_type, 'HEAD')
|
||||
extra_vars.update({
|
||||
'project_path': project_update.get_project_path(check_if_exists=False),
|
||||
'projects_root': settings.PROJECTS_ROOT.rstrip('/'),
|
||||
'local_path': os.path.basename(project_update.project.local_path),
|
||||
'project_path': project_update.get_project_path(check_if_exists=False), # deprecated
|
||||
'insights_url': settings.INSIGHTS_URL_BASE,
|
||||
'awx_license_type': get_license(show_key=False).get('license_type', 'UNLICENSED'),
|
||||
'awx_version': get_awx_version(),
|
||||
@@ -2184,9 +2160,6 @@ class RunProjectUpdate(BaseTask):
|
||||
'roles_enabled': settings.AWX_ROLES_ENABLED,
|
||||
'collections_enabled': settings.AWX_COLLECTIONS_ENABLED,
|
||||
})
|
||||
if project_update.job_type != 'check' and self.job_private_data_dir:
|
||||
extra_vars['collections_destination'] = os.path.join(self.job_private_data_dir, 'requirements_collections')
|
||||
extra_vars['roles_destination'] = os.path.join(self.job_private_data_dir, 'requirements_roles')
|
||||
# apply custom refspec from user for PR refs and the like
|
||||
if project_update.scm_refspec:
|
||||
extra_vars['scm_refspec'] = project_update.scm_refspec
|
||||
@@ -2322,8 +2295,7 @@ class RunProjectUpdate(BaseTask):
|
||||
os.mkdir(settings.PROJECTS_ROOT)
|
||||
self.acquire_lock(instance)
|
||||
self.original_branch = None
|
||||
if (instance.scm_type == 'git' and instance.job_type == 'run' and instance.project and
|
||||
instance.scm_branch != instance.project.scm_branch):
|
||||
if instance.scm_type == 'git' and instance.branch_override:
|
||||
project_path = instance.project.get_project_path(check_if_exists=False)
|
||||
if os.path.exists(project_path):
|
||||
git_repo = git.Repo(project_path)
|
||||
@@ -2332,17 +2304,48 @@ class RunProjectUpdate(BaseTask):
|
||||
else:
|
||||
self.original_branch = git_repo.active_branch
|
||||
|
||||
stage_path = os.path.join(instance.get_cache_path(), 'stage')
|
||||
if os.path.exists(stage_path):
|
||||
logger.warning('{0} unexpectedly existed before update'.format(stage_path))
|
||||
shutil.rmtree(stage_path)
|
||||
os.makedirs(stage_path) # presence of empty cache indicates lack of roles or collections
|
||||
|
||||
@staticmethod
|
||||
def make_local_copy(project_path, destination_folder, scm_type, scm_revision):
|
||||
if scm_type == 'git':
|
||||
def clear_project_cache(cache_dir, keep_value):
|
||||
if os.path.isdir(cache_dir):
|
||||
for entry in os.listdir(cache_dir):
|
||||
old_path = os.path.join(cache_dir, entry)
|
||||
if entry not in (keep_value, 'stage'):
|
||||
# invalidate, then delete
|
||||
new_path = os.path.join(cache_dir,'.~~delete~~' + entry)
|
||||
try:
|
||||
os.rename(old_path, new_path)
|
||||
shutil.rmtree(new_path)
|
||||
except OSError:
|
||||
logger.warning(f"Could not remove cache directory {old_path}")
|
||||
|
||||
@staticmethod
|
||||
def make_local_copy(p, job_private_data_dir, scm_revision=None):
|
||||
"""Copy project content (roles and collections) to a job private_data_dir
|
||||
|
||||
:param object p: Either a project or a project update
|
||||
:param str job_private_data_dir: The root of the target ansible-runner folder
|
||||
:param str scm_revision: For branch_override cases, the git revision to copy
|
||||
"""
|
||||
project_path = p.get_project_path(check_if_exists=False)
|
||||
destination_folder = os.path.join(job_private_data_dir, 'project')
|
||||
if not scm_revision:
|
||||
scm_revision = p.scm_revision
|
||||
|
||||
if p.scm_type == 'git':
|
||||
git_repo = git.Repo(project_path)
|
||||
if not os.path.exists(destination_folder):
|
||||
os.mkdir(destination_folder, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
|
||||
tmp_branch_name = 'awx_internal/{}'.format(uuid4())
|
||||
# always clone based on specific job revision
|
||||
if not scm_revision:
|
||||
if not p.scm_revision:
|
||||
raise RuntimeError('Unexpectedly could not determine a revision to run from project.')
|
||||
source_branch = git_repo.create_head(tmp_branch_name, scm_revision)
|
||||
source_branch = git_repo.create_head(tmp_branch_name, p.scm_revision)
|
||||
# git clone must take file:// syntax for source repo or else options like depth will be ignored
|
||||
source_as_uri = Path(project_path).as_uri()
|
||||
git.Repo.clone_from(
|
||||
@@ -2361,19 +2364,48 @@ class RunProjectUpdate(BaseTask):
|
||||
else:
|
||||
copy_tree(project_path, destination_folder, preserve_symlinks=1)
|
||||
|
||||
# copy over the roles and collection cache to job folder
|
||||
cache_path = os.path.join(p.get_cache_path(), p.cache_id)
|
||||
subfolders = []
|
||||
if settings.AWX_COLLECTIONS_ENABLED:
|
||||
subfolders.append('requirements_collections')
|
||||
if settings.AWX_ROLES_ENABLED:
|
||||
subfolders.append('requirements_roles')
|
||||
for subfolder in subfolders:
|
||||
cache_subpath = os.path.join(cache_path, subfolder)
|
||||
if os.path.exists(cache_subpath):
|
||||
dest_subpath = os.path.join(job_private_data_dir, subfolder)
|
||||
copy_tree(cache_subpath, dest_subpath, preserve_symlinks=1)
|
||||
logger.debug('{0} {1} prepared {2} from cache'.format(type(p).__name__, p.pk, dest_subpath))
|
||||
|
||||
def post_run_hook(self, instance, status):
|
||||
# To avoid hangs, very important to release lock even if errors happen here
|
||||
try:
|
||||
if self.playbook_new_revision:
|
||||
instance.scm_revision = self.playbook_new_revision
|
||||
instance.save(update_fields=['scm_revision'])
|
||||
|
||||
# Roles and collection folders copy to durable cache
|
||||
base_path = instance.get_cache_path()
|
||||
stage_path = os.path.join(base_path, 'stage')
|
||||
if status == 'successful' and 'install_' in instance.job_tags:
|
||||
# Clear other caches before saving this one, and if branch is overridden
|
||||
# do not clear cache for main branch, but do clear it for other branches
|
||||
self.clear_project_cache(base_path, keep_value=instance.project.cache_id)
|
||||
cache_path = os.path.join(base_path, instance.cache_id)
|
||||
if os.path.exists(stage_path):
|
||||
if os.path.exists(cache_path):
|
||||
logger.warning('Rewriting cache at {0}, performance may suffer'.format(cache_path))
|
||||
shutil.rmtree(cache_path)
|
||||
os.rename(stage_path, cache_path)
|
||||
logger.debug('{0} wrote to cache at {1}'.format(instance.log_format, cache_path))
|
||||
elif os.path.exists(stage_path):
|
||||
shutil.rmtree(stage_path) # cannot trust content update produced
|
||||
|
||||
if self.job_private_data_dir:
|
||||
# copy project folder before resetting to default branch
|
||||
# because some git-tree-specific resources (like submodules) might matter
|
||||
self.make_local_copy(
|
||||
instance.get_project_path(check_if_exists=False), os.path.join(self.job_private_data_dir, 'project'),
|
||||
instance.scm_type, instance.scm_revision
|
||||
)
|
||||
self.make_local_copy(instance, self.job_private_data_dir)
|
||||
if self.original_branch:
|
||||
# for git project syncs, non-default branches can be problems
|
||||
# restore to branch the repo was on before this run
|
||||
@@ -2472,7 +2504,7 @@ class RunInventoryUpdate(BaseTask):
|
||||
|
||||
if inventory_update.source in ['scm', 'custom']:
|
||||
for env_k in inventory_update.source_vars_dict:
|
||||
if str(env_k) not in env and str(env_k) not in settings.INV_ENV_VARIABLE_BLACKLIST:
|
||||
if str(env_k) not in env and str(env_k) not in settings.INV_ENV_VARIABLE_BLOCKED:
|
||||
env[str(env_k)] = str(inventory_update.source_vars_dict[env_k])
|
||||
elif inventory_update.source == 'file':
|
||||
raise NotImplementedError('Cannot update file sources through the task system.')
|
||||
@@ -2626,13 +2658,21 @@ class RunInventoryUpdate(BaseTask):
|
||||
source_project = None
|
||||
if inventory_update.inventory_source:
|
||||
source_project = inventory_update.inventory_source.source_project
|
||||
if (inventory_update.source=='scm' and inventory_update.launch_type!='scm' and source_project):
|
||||
# In project sync, pulling galaxy roles is not needed
|
||||
if (inventory_update.source=='scm' and inventory_update.launch_type!='scm' and
|
||||
source_project and source_project.scm_type): # never ever update manual projects
|
||||
|
||||
# Check if the content cache exists, so that we do not unnecessarily re-download roles
|
||||
sync_needs = ['update_{}'.format(source_project.scm_type)]
|
||||
has_cache = os.path.exists(os.path.join(source_project.get_cache_path(), source_project.cache_id))
|
||||
# Galaxy requirements are not supported for manual projects
|
||||
if not has_cache:
|
||||
sync_needs.extend(['install_roles', 'install_collections'])
|
||||
|
||||
local_project_sync = source_project.create_project_update(
|
||||
_eager_fields=dict(
|
||||
launch_type="sync",
|
||||
job_type='run',
|
||||
job_tags='update_{},install_collections'.format(source_project.scm_type), # roles are never valid for inventory
|
||||
job_tags=','.join(sync_needs),
|
||||
status='running',
|
||||
execution_node=inventory_update.execution_node,
|
||||
instance_group = inventory_update.instance_group,
|
||||
@@ -2656,11 +2696,7 @@ class RunInventoryUpdate(BaseTask):
|
||||
raise
|
||||
elif inventory_update.source == 'scm' and inventory_update.launch_type == 'scm' and source_project:
|
||||
# This follows update, not sync, so make copy here
|
||||
project_path = source_project.get_project_path(check_if_exists=False)
|
||||
RunProjectUpdate.make_local_copy(
|
||||
project_path, os.path.join(private_data_dir, 'project'),
|
||||
source_project.scm_type, source_project.scm_revision
|
||||
)
|
||||
RunProjectUpdate.make_local_copy(source_project, private_data_dir)
|
||||
|
||||
|
||||
@task(queue=get_local_queuename)
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
"TOWER_HOST": "https://foo.invalid",
|
||||
"TOWER_PASSWORD": "fooo",
|
||||
"TOWER_USERNAME": "fooo",
|
||||
"TOWER_OAUTH_TOKEN": "",
|
||||
"TOWER_VERIFY_SSL": "False"
|
||||
}
|
||||
@@ -1153,6 +1153,22 @@ def test_cloud_credential_type_mutability(patch, organization, admin, credential
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('field', ['password', 'ssh_key_data'])
|
||||
def test_secret_fields_cannot_be_special_encrypted_variable(post, organization, admin, credentialtype_ssh, field):
|
||||
params = {
|
||||
'name': 'Best credential ever',
|
||||
'credential_type': credentialtype_ssh.id,
|
||||
'inputs': {
|
||||
'username': 'joe',
|
||||
field: '$encrypted$',
|
||||
},
|
||||
'organization': organization.id,
|
||||
}
|
||||
response = post(reverse('api:credential_list'), params, admin, status=400)
|
||||
assert str(response.data['inputs'][0]) == f'$encrypted$ is a reserved keyword, and cannot be used for {field}.'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_ssh_unlock_needed(put, organization, admin, credentialtype_ssh):
|
||||
params = {
|
||||
|
||||
@@ -220,7 +220,7 @@ def test_create_valid_kind(kind, get, post, admin):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('kind', ['ssh', 'vault', 'scm', 'insights'])
|
||||
@pytest.mark.parametrize('kind', ['ssh', 'vault', 'scm', 'insights', 'kubernetes'])
|
||||
def test_create_invalid_kind(kind, get, post, admin):
|
||||
response = post(reverse('api:credential_type_list'), {
|
||||
'kind': kind,
|
||||
|
||||
@@ -4,7 +4,7 @@ from awx.api.versioning import reverse
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_proxy_ip_whitelist(get, patch, admin):
|
||||
def test_proxy_ip_allowed(get, patch, admin):
|
||||
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'system'})
|
||||
patch(url, user=admin, data={
|
||||
'REMOTE_HOST_HEADERS': [
|
||||
@@ -23,37 +23,37 @@ def test_proxy_ip_whitelist(get, patch, admin):
|
||||
def process_response(self, request, response):
|
||||
self.environ = request.environ
|
||||
|
||||
# By default, `PROXY_IP_WHITELIST` is disabled, so custom `REMOTE_HOST_HEADERS`
|
||||
# By default, `PROXY_IP_ALLOWED_LIST` is disabled, so custom `REMOTE_HOST_HEADERS`
|
||||
# should just pass through
|
||||
middleware = HeaderTrackingMiddleware()
|
||||
get(url, user=admin, middleware=middleware,
|
||||
HTTP_X_FROM_THE_LOAD_BALANCER='some-actual-ip')
|
||||
assert middleware.environ['HTTP_X_FROM_THE_LOAD_BALANCER'] == 'some-actual-ip'
|
||||
|
||||
# If `PROXY_IP_WHITELIST` is restricted to 10.0.1.100 and we make a request
|
||||
# If `PROXY_IP_ALLOWED_LIST` is restricted to 10.0.1.100 and we make a request
|
||||
# from 8.9.10.11, the custom `HTTP_X_FROM_THE_LOAD_BALANCER` header should
|
||||
# be stripped
|
||||
patch(url, user=admin, data={
|
||||
'PROXY_IP_WHITELIST': ['10.0.1.100']
|
||||
'PROXY_IP_ALLOWED_LIST': ['10.0.1.100']
|
||||
})
|
||||
middleware = HeaderTrackingMiddleware()
|
||||
get(url, user=admin, middleware=middleware, REMOTE_ADDR='8.9.10.11',
|
||||
HTTP_X_FROM_THE_LOAD_BALANCER='some-actual-ip')
|
||||
assert 'HTTP_X_FROM_THE_LOAD_BALANCER' not in middleware.environ
|
||||
|
||||
# If 8.9.10.11 is added to `PROXY_IP_WHITELIST` the
|
||||
# If 8.9.10.11 is added to `PROXY_IP_ALLOWED_LIST` the
|
||||
# `HTTP_X_FROM_THE_LOAD_BALANCER` header should be passed through again
|
||||
patch(url, user=admin, data={
|
||||
'PROXY_IP_WHITELIST': ['10.0.1.100', '8.9.10.11']
|
||||
'PROXY_IP_ALLOWED_LIST': ['10.0.1.100', '8.9.10.11']
|
||||
})
|
||||
middleware = HeaderTrackingMiddleware()
|
||||
get(url, user=admin, middleware=middleware, REMOTE_ADDR='8.9.10.11',
|
||||
HTTP_X_FROM_THE_LOAD_BALANCER='some-actual-ip')
|
||||
assert middleware.environ['HTTP_X_FROM_THE_LOAD_BALANCER'] == 'some-actual-ip'
|
||||
|
||||
# Allow whitelisting of proxy hostnames in addition to IP addresses
|
||||
# Allow allowed list of proxy hostnames in addition to IP addresses
|
||||
patch(url, user=admin, data={
|
||||
'PROXY_IP_WHITELIST': ['my.proxy.example.org']
|
||||
'PROXY_IP_ALLOWED_LIST': ['my.proxy.example.org']
|
||||
})
|
||||
middleware = HeaderTrackingMiddleware()
|
||||
get(url, user=admin, middleware=middleware, REMOTE_ADDR='8.9.10.11',
|
||||
|
||||
@@ -60,6 +60,42 @@ def test_inventory_source_unique_together_with_inv(inventory_factory):
|
||||
is2.validate_unique()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_host_name_unique(scm_inventory, post, admin_user):
|
||||
inv_src = scm_inventory.inventory_sources.first()
|
||||
inv_src.groups.create(name='barfoo', inventory=scm_inventory)
|
||||
resp = post(
|
||||
reverse('api:inventory_hosts_list', kwargs={'pk': scm_inventory.id}),
|
||||
{
|
||||
'name': 'barfoo',
|
||||
'inventory_id': scm_inventory.id,
|
||||
},
|
||||
admin_user,
|
||||
expect=400
|
||||
)
|
||||
|
||||
assert resp.status_code == 400
|
||||
assert "A Group with that name already exists." in json.dumps(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_group_name_unique(scm_inventory, post, admin_user):
|
||||
inv_src = scm_inventory.inventory_sources.first()
|
||||
inv_src.hosts.create(name='barfoo', inventory=scm_inventory)
|
||||
resp = post(
|
||||
reverse('api:inventory_groups_list', kwargs={'pk': scm_inventory.id}),
|
||||
{
|
||||
'name': 'barfoo',
|
||||
'inventory_id': scm_inventory.id,
|
||||
},
|
||||
admin_user,
|
||||
expect=400
|
||||
)
|
||||
|
||||
assert resp.status_code == 400
|
||||
assert "A Host with that name already exists." in json.dumps(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role_field,expected_status_code", [
|
||||
(None, 403),
|
||||
('admin_role', 200),
|
||||
@@ -413,7 +449,7 @@ def test_inventory_update_access_called(post, inventory_source, alice, mock_acce
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_source_vars_prohibition(post, inventory, admin_user):
|
||||
with mock.patch('awx.api.serializers.settings') as mock_settings:
|
||||
mock_settings.INV_ENV_VARIABLE_BLACKLIST = ('FOOBAR',)
|
||||
mock_settings.INV_ENV_VARIABLE_BLOCKED = ('FOOBAR',)
|
||||
r = post(reverse('api:inventory_source_list'),
|
||||
{'name': 'new inv src', 'source_vars': '{\"FOOBAR\": \"val\"}', 'inventory': inventory.pk},
|
||||
admin_user, expect=400)
|
||||
|
||||
@@ -483,25 +483,26 @@ def test_job_launch_pass_with_prompted_vault_password(machine_credential, vault_
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_job_launch_JT_with_credentials(machine_credential, credential, net_credential, deploy_jobtemplate):
|
||||
def test_job_launch_JT_with_credentials(machine_credential, credential, net_credential, kube_credential, deploy_jobtemplate):
|
||||
deploy_jobtemplate.ask_credential_on_launch = True
|
||||
deploy_jobtemplate.save()
|
||||
|
||||
kv = dict(credentials=[credential.pk, net_credential.pk, machine_credential.pk])
|
||||
kv = dict(credentials=[credential.pk, net_credential.pk, machine_credential.pk, kube_credential.pk])
|
||||
serializer = JobLaunchSerializer(data=kv, context={'template': deploy_jobtemplate})
|
||||
validated = serializer.is_valid()
|
||||
assert validated, serializer.errors
|
||||
|
||||
kv['credentials'] = [credential, net_credential, machine_credential] # convert to internal value
|
||||
kv['credentials'] = [credential, net_credential, machine_credential, kube_credential] # convert to internal value
|
||||
prompted_fields, ignored_fields, errors = deploy_jobtemplate._accept_or_ignore_job_kwargs(
|
||||
_exclude_errors=['required', 'prompts'], **kv)
|
||||
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
||||
|
||||
creds = job_obj.credentials.all()
|
||||
assert len(creds) == 3
|
||||
assert len(creds) == 4
|
||||
assert credential in creds
|
||||
assert net_credential in creds
|
||||
assert machine_credential in creds
|
||||
assert kube_credential in creds
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -54,7 +54,9 @@ def test_no_changing_overwrite_behavior_if_used(post, patch, organization, admin
|
||||
data={
|
||||
'name': 'fooo',
|
||||
'organization': organization.id,
|
||||
'allow_override': True
|
||||
'allow_override': True,
|
||||
'scm_type': 'git',
|
||||
'scm_url': 'https://github.com/ansible/test-playbooks.git'
|
||||
},
|
||||
user=admin_user,
|
||||
expect=201
|
||||
@@ -83,7 +85,9 @@ def test_changing_overwrite_behavior_okay_if_not_used(post, patch, organization,
|
||||
data={
|
||||
'name': 'fooo',
|
||||
'organization': organization.id,
|
||||
'allow_override': True
|
||||
'allow_override': True,
|
||||
'scm_type': 'git',
|
||||
'scm_url': 'https://github.com/ansible/test-playbooks.git'
|
||||
},
|
||||
user=admin_user,
|
||||
expect=201
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
@@ -61,3 +63,21 @@ def test_user_cannot_update_last_login(patch, admin):
|
||||
middleware=SessionMiddleware()
|
||||
)
|
||||
assert User.objects.get(pk=admin.pk).last_login is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_verify_attribute_created(admin, get):
|
||||
assert admin.created == admin.date_joined
|
||||
resp = get(
|
||||
reverse('api:user_detail', kwargs={'pk': admin.pk}),
|
||||
admin
|
||||
)
|
||||
assert resp.data['created'] == admin.date_joined
|
||||
|
||||
past = date(2020, 1, 1).isoformat()
|
||||
for op, count in (('gt', 1), ('lt', 0)):
|
||||
resp = get(
|
||||
reverse('api:user_list') + f'?created__{op}={past}',
|
||||
admin
|
||||
)
|
||||
assert resp.data['count'] == count
|
||||
|
||||
@@ -145,7 +145,6 @@ def project(instance, organization):
|
||||
description="test-proj-desc",
|
||||
organization=organization,
|
||||
playbook_files=['helloworld.yml', 'alt-helloworld.yml'],
|
||||
local_path='_92__test_proj',
|
||||
scm_revision='1234567890123456789012345678901234567890',
|
||||
scm_url='localhost',
|
||||
scm_type='git'
|
||||
|
||||
@@ -169,7 +169,8 @@ class TestSCMUpdateFeatures:
|
||||
inventory_update = InventoryUpdate(
|
||||
inventory_source=scm_inventory_source,
|
||||
source_path=scm_inventory_source.source_path)
|
||||
assert inventory_update.get_actual_source_path().endswith('_92__test_proj/inventory_file')
|
||||
p = scm_inventory_source.source_project
|
||||
assert inventory_update.get_actual_source_path().endswith(f'_{p.id}__test_proj/inventory_file')
|
||||
|
||||
def test_no_unwanted_updates(self, scm_inventory_source):
|
||||
# Changing the non-sensitive fields should not trigger update
|
||||
|
||||
@@ -12,6 +12,7 @@ from awx.api.serializers import UnifiedJobSerializer
|
||||
|
||||
class TestJobNotificationMixin(object):
|
||||
CONTEXT_STRUCTURE = {'job': {'allow_simultaneous': bool,
|
||||
'artifacts': {},
|
||||
'custom_virtualenv': str,
|
||||
'controller_node': str,
|
||||
'created': datetime.datetime,
|
||||
|
||||
@@ -34,6 +34,18 @@ def test_sensitive_change_triggers_update(project):
|
||||
mock_update.assert_called_once_with()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_local_path_autoset(organization):
|
||||
with mock.patch.object(Project, "update"):
|
||||
p = Project.objects.create(
|
||||
name="test-proj",
|
||||
organization=organization,
|
||||
scm_url='localhost',
|
||||
scm_type='git'
|
||||
)
|
||||
assert p.local_path == f'_{p.id}__test_proj'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_foreign_key_change_changes_modified_by(project, organization):
|
||||
assert project._get_fields_snapshot()['organization_id'] == organization.id
|
||||
|
||||
@@ -124,6 +124,9 @@ def fake_credential_factory():
|
||||
for var in var_specs.keys():
|
||||
inputs[var] = generate_fake_var(var_specs[var])
|
||||
|
||||
if source == 'tower':
|
||||
inputs.pop('oauth_token') # mutually exclusive with user/pass
|
||||
|
||||
return Credential.objects.create(
|
||||
credential_type=ct,
|
||||
inputs=inputs
|
||||
|
||||
@@ -2,7 +2,9 @@ import pytest
|
||||
from unittest import mock
|
||||
import json
|
||||
|
||||
from awx.main.models import Job, Instance, JobHostSummary
|
||||
from awx.main.models import (Job, Instance, JobHostSummary, InventoryUpdate,
|
||||
InventorySource, Project, ProjectUpdate,
|
||||
SystemJob, AdHocCommand)
|
||||
from awx.main.tasks import cluster_node_heartbeat
|
||||
from django.test.utils import override_settings
|
||||
|
||||
@@ -33,6 +35,31 @@ def test_job_capacity_and_with_inactive_node():
|
||||
assert i.capacity == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_job_type_name():
|
||||
job = Job.objects.create()
|
||||
assert job.job_type_name == 'job'
|
||||
|
||||
ahc = AdHocCommand.objects.create()
|
||||
assert ahc.job_type_name == 'ad_hoc_command'
|
||||
|
||||
source = InventorySource.objects.create(source='ec2')
|
||||
source.save()
|
||||
iu = InventoryUpdate.objects.create(
|
||||
inventory_source=source,
|
||||
source='ec2'
|
||||
)
|
||||
assert iu.job_type_name == 'inventory_update'
|
||||
|
||||
proj = Project.objects.create()
|
||||
proj.save()
|
||||
pu = ProjectUpdate.objects.create(project=proj)
|
||||
assert pu.job_type_name == 'project_update'
|
||||
|
||||
sjob = SystemJob.objects.create()
|
||||
assert sjob.job_type_name == 'system_job'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_job_notification_data(inventory, machine_credential, project):
|
||||
encrypted_str = "$encrypted$"
|
||||
|
||||
@@ -219,3 +219,27 @@ def test_credential(get, admin_user, credentialtype_ssh):
|
||||
url = reverse('api:credential_detail', kwargs={'pk': test_cred.pk})
|
||||
response = get(url, user=admin_user, expect=200)
|
||||
assert response.data['related']['named_url'].endswith('/test_cred++Machine+ssh++/')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_403_vs_404(get):
|
||||
cindy = User.objects.create(
|
||||
username='cindy',
|
||||
password='test_user',
|
||||
is_superuser=False
|
||||
)
|
||||
bob = User.objects.create(
|
||||
username='bob',
|
||||
password='test_user',
|
||||
is_superuser=False
|
||||
)
|
||||
|
||||
# bob cannot see cindy, pk lookup should be a 403
|
||||
url = reverse('api:user_detail', kwargs={'pk': cindy.pk})
|
||||
get(url, user=bob, expect=403)
|
||||
|
||||
# bob cannot see cindy, username lookup should be a 404
|
||||
get('/api/v2/users/cindy/', user=bob, expect=404)
|
||||
|
||||
get(f'/api/v2/users/{cindy.pk}/', expect=401)
|
||||
get('/api/v2/users/cindy/', expect=404)
|
||||
|
||||
@@ -29,8 +29,8 @@ def team_project_list(organization_factory):
|
||||
@pytest.mark.django_db
|
||||
def test_get_project_path(project):
|
||||
# Test combining projects root with project local path
|
||||
with mock.patch('awx.main.models.projects.settings.PROJECTS_ROOT', '/var/lib/awx'):
|
||||
assert project.get_project_path(check_if_exists=False) == '/var/lib/awx/_92__test_proj'
|
||||
with mock.patch('awx.main.models.projects.settings.PROJECTS_ROOT', '/var/lib/foo'):
|
||||
assert project.get_project_path(check_if_exists=False) == f'/var/lib/foo/_{project.id}__test_proj'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -20,8 +20,19 @@ def test_label_get_queryset_su(label, user):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_label_access(label, user):
|
||||
def test_label_read_access(label, user):
|
||||
access = LabelAccess(user('user', False))
|
||||
assert not access.can_read(label)
|
||||
label.organization.member_role.members.add(user('user', False))
|
||||
assert access.can_read(label)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_label_jt_read_access(label, user, job_template):
|
||||
access = LabelAccess(user('user', False))
|
||||
assert not access.can_read(label)
|
||||
job_template.read_role.members.add(user('user', False))
|
||||
job_template.labels.add(label)
|
||||
assert access.can_read(label)
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestDependentInventoryUpdate:
|
||||
def test_dependent_inventory_updates_is_called(self, scm_inventory_source, scm_revision_file):
|
||||
task = RunProjectUpdate()
|
||||
task.revision_path = scm_revision_file
|
||||
proj_update = ProjectUpdate.objects.create(project=scm_inventory_source.source_project)
|
||||
proj_update = scm_inventory_source.source_project.create_project_update()
|
||||
with mock.patch.object(RunProjectUpdate, '_update_dependent_inventories') as inv_update_mck:
|
||||
with mock.patch.object(RunProjectUpdate, 'release_lock'):
|
||||
task.post_run_hook(proj_update, 'successful')
|
||||
@@ -39,7 +39,7 @@ class TestDependentInventoryUpdate:
|
||||
def test_no_unwanted_dependent_inventory_updates(self, project, scm_revision_file):
|
||||
task = RunProjectUpdate()
|
||||
task.revision_path = scm_revision_file
|
||||
proj_update = ProjectUpdate.objects.create(project=project)
|
||||
proj_update = project.create_project_update()
|
||||
with mock.patch.object(RunProjectUpdate, '_update_dependent_inventories') as inv_update_mck:
|
||||
with mock.patch.object(RunProjectUpdate, 'release_lock'):
|
||||
task.post_run_hook(proj_update, 'successful')
|
||||
|
||||
@@ -61,7 +61,10 @@ def patch_Job():
|
||||
|
||||
@pytest.fixture
|
||||
def job():
|
||||
return Job(pk=1, id=1, project=Project(), inventory=Inventory(), job_template=JobTemplate(id=1, name='foo'))
|
||||
return Job(
|
||||
pk=1, id=1,
|
||||
project=Project(local_path='/projects/_23_foo'),
|
||||
inventory=Inventory(), job_template=JobTemplate(id=1, name='foo'))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -347,7 +350,7 @@ class TestExtraVarSanitation(TestJobExecution):
|
||||
assert extra_vars['msg'] == {'a': [self.UNSAFE]}
|
||||
assert hasattr(extra_vars['msg']['a'][0], '__UNSAFE__')
|
||||
|
||||
def test_whitelisted_jt_extra_vars(self, job, private_data_dir):
|
||||
def test_allowed_jt_extra_vars(self, job, private_data_dir):
|
||||
job.job_template.extra_vars = job.extra_vars = json.dumps({'msg': self.UNSAFE})
|
||||
task = tasks.RunJob()
|
||||
|
||||
@@ -358,7 +361,7 @@ class TestExtraVarSanitation(TestJobExecution):
|
||||
assert extra_vars['msg'] == self.UNSAFE
|
||||
assert not hasattr(extra_vars['msg'], '__UNSAFE__')
|
||||
|
||||
def test_nested_whitelisted_vars(self, job, private_data_dir):
|
||||
def test_nested_allowed_vars(self, job, private_data_dir):
|
||||
job.extra_vars = json.dumps({'msg': {'a': {'b': [self.UNSAFE]}}})
|
||||
job.job_template.extra_vars = job.extra_vars
|
||||
task = tasks.RunJob()
|
||||
@@ -406,7 +409,9 @@ class TestExtraVarSanitation(TestJobExecution):
|
||||
class TestGenericRun():
|
||||
|
||||
def test_generic_failure(self, patch_Job):
|
||||
job = Job(status='running', inventory=Inventory(), project=Project())
|
||||
job = Job(
|
||||
status='running', inventory=Inventory(),
|
||||
project=Project(local_path='/projects/_23_foo'))
|
||||
job.websocket_emit_status = mock.Mock()
|
||||
|
||||
task = tasks.RunJob()
|
||||
@@ -1037,6 +1042,43 @@ class TestJobCredentials(TestJobExecution):
|
||||
assert '--vault-id dev@prompt' in ' '.join(args)
|
||||
assert '--vault-id prod@prompt' in ' '.join(args)
|
||||
|
||||
@pytest.mark.parametrize("verify", (True, False))
|
||||
def test_k8s_credential(self, job, private_data_dir, verify):
|
||||
k8s = CredentialType.defaults['kubernetes_bearer_token']()
|
||||
inputs = {
|
||||
'host': 'https://example.org/',
|
||||
'bearer_token': 'token123',
|
||||
}
|
||||
if verify:
|
||||
inputs['verify_ssl'] = True
|
||||
inputs['ssl_ca_cert'] = 'CERTDATA'
|
||||
credential = Credential(
|
||||
pk=1,
|
||||
credential_type=k8s,
|
||||
inputs = inputs,
|
||||
)
|
||||
credential.inputs['bearer_token'] = encrypt_field(credential, 'bearer_token')
|
||||
job.credentials.add(credential)
|
||||
|
||||
env = {}
|
||||
safe_env = {}
|
||||
credential.credential_type.inject_credential(
|
||||
credential, env, safe_env, [], private_data_dir
|
||||
)
|
||||
|
||||
assert env['K8S_AUTH_HOST'] == 'https://example.org/'
|
||||
assert env['K8S_AUTH_API_KEY'] == 'token123'
|
||||
|
||||
if verify:
|
||||
assert env['K8S_AUTH_VERIFY_SSL'] == 'True'
|
||||
cert = open(env['K8S_AUTH_SSL_CA_CERT'], 'r').read()
|
||||
assert cert == 'CERTDATA'
|
||||
else:
|
||||
assert env['K8S_AUTH_VERIFY_SSL'] == 'False'
|
||||
assert 'K8S_AUTH_SSL_CA_CERT' not in env
|
||||
|
||||
assert safe_env['K8S_AUTH_API_KEY'] == tasks.HIDDEN_PASSWORD
|
||||
|
||||
def test_aws_cloud_credential(self, job, private_data_dir):
|
||||
aws = CredentialType.defaults['aws']()
|
||||
credential = Credential(
|
||||
|
||||
@@ -367,13 +367,13 @@ def get_allowed_fields(obj, serializer_mapping):
|
||||
'oauth2application': ['client_secret']
|
||||
}
|
||||
model_name = obj._meta.model_name
|
||||
field_blacklist = ACTIVITY_STREAM_FIELD_EXCLUSIONS.get(model_name, [])
|
||||
fields_excluded = ACTIVITY_STREAM_FIELD_EXCLUSIONS.get(model_name, [])
|
||||
# see definition of from_db for CredentialType
|
||||
# injection logic of any managed types are incompatible with activity stream
|
||||
if model_name == 'credentialtype' and obj.managed_by_tower and obj.namespace:
|
||||
field_blacklist.extend(['inputs', 'injectors'])
|
||||
if field_blacklist:
|
||||
allowed_fields = [f for f in allowed_fields if f not in field_blacklist]
|
||||
fields_excluded.extend(['inputs', 'injectors'])
|
||||
if fields_excluded:
|
||||
allowed_fields = [f for f in allowed_fields if f not in fields_excluded]
|
||||
return allowed_fields
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from django.apps import apps
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
from awx.main.constants import LOGGER_BLACKLIST
|
||||
from awx.main.constants import LOGGER_BLOCKLIST
|
||||
from awx.main.utils.common import get_search_fields
|
||||
|
||||
__all__ = ['SmartFilter', 'ExternalLoggerEnabled', 'DynamicLevelFilter']
|
||||
@@ -48,11 +48,11 @@ class FieldFromSettings(object):
|
||||
instance.settings_override[self.setting_name] = value
|
||||
|
||||
|
||||
def record_is_blacklisted(record):
|
||||
"""Given a log record, return True if it is considered to be in
|
||||
the logging blacklist, return False if not
|
||||
def record_is_blocked(record):
|
||||
"""Given a log record, return True if it is considered to be
|
||||
blocked, return False if not
|
||||
"""
|
||||
for logger_name in LOGGER_BLACKLIST:
|
||||
for logger_name in LOGGER_BLOCKLIST:
|
||||
if record.name.startswith(logger_name):
|
||||
return True
|
||||
return False
|
||||
@@ -81,7 +81,7 @@ class ExternalLoggerEnabled(Filter):
|
||||
True - should be logged
|
||||
"""
|
||||
# Do not send exceptions to external logger
|
||||
if record_is_blacklisted(record):
|
||||
if record_is_blocked(record):
|
||||
return False
|
||||
# General enablement
|
||||
if not self.enabled_flag:
|
||||
@@ -108,8 +108,8 @@ class DynamicLevelFilter(Filter):
|
||||
"""Filters out logs that have a level below the threshold defined
|
||||
by the databse setting LOG_AGGREGATOR_LEVEL
|
||||
"""
|
||||
if record_is_blacklisted(record):
|
||||
# Fine to write blacklisted loggers to file, apply default filtering level
|
||||
if record_is_blocked(record):
|
||||
# Fine to write denied loggers to file, apply default filtering level
|
||||
cutoff_level = logging.WARNING
|
||||
else:
|
||||
try:
|
||||
@@ -179,7 +179,7 @@ class SmartFilter(object):
|
||||
pyparsing do the heavy lifting.
|
||||
TODO: separate django filter requests from our custom json filter
|
||||
request so we don't process the key any. This could be
|
||||
accomplished using a whitelist or introspecting the
|
||||
accomplished using an allowed list or introspecting the
|
||||
relationship refered to to see if it's a jsonb type.
|
||||
'''
|
||||
def _json_path_to_contains(self, k, v):
|
||||
|
||||
@@ -36,7 +36,7 @@ def safe_dump(x, safe_dict=None):
|
||||
_unless_ they've been deemed "trusted" (meaning, they likely were set/added
|
||||
by a user with a high level of privilege).
|
||||
|
||||
This function allows you to pass in a trusted `safe_dict` to whitelist
|
||||
This function allows you to pass in a trusted `safe_dict` to allow
|
||||
certain extra vars so that they are _not_ marked as `!unsafe` in the
|
||||
resulting YAML. Anything _not_ in this dict will automatically be
|
||||
`!unsafe`.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
---
|
||||
# The following variables will be set by the runner of this playbook:
|
||||
# project_path: PROJECTS_DIR/_local_path_
|
||||
# projects_root: Global location for caching project checkouts and roles and collections
|
||||
# should not have trailing slash on end
|
||||
# local_path: Path within projects_root to use for this project
|
||||
# project_path: A simple join of projects_root/local_path folders
|
||||
# scm_url: https://server/repo
|
||||
# insights_url: Insights service URL (from configuration)
|
||||
# scm_branch: branch/tag/revision (HEAD if unset)
|
||||
@@ -11,8 +14,6 @@
|
||||
# scm_refspec: a refspec to fetch in addition to obtaining version
|
||||
# roles_enabled: Value of the global setting to enable roles downloading
|
||||
# collections_enabled: Value of the global setting to enable collections downloading
|
||||
# roles_destination: Path to save roles from galaxy to
|
||||
# collections_destination: Path to save collections from galaxy to
|
||||
# awx_version: Current running version of the awx or tower as a string
|
||||
# awx_license_type: "open" for AWX; else presume Tower
|
||||
|
||||
@@ -122,7 +123,10 @@
|
||||
register: doesRequirementsExist
|
||||
|
||||
- name: fetch galaxy roles from requirements.yml
|
||||
command: ansible-galaxy role install -r roles/requirements.yml -p {{roles_destination|quote}}{{ ' -' + 'v' * ansible_verbosity if ansible_verbosity else '' }}
|
||||
command: >
|
||||
ansible-galaxy role install -r roles/requirements.yml
|
||||
--roles-path {{projects_root}}/.__awx_cache/{{local_path}}/stage/requirements_roles
|
||||
{{ ' -' + 'v' * ansible_verbosity if ansible_verbosity else '' }}
|
||||
args:
|
||||
chdir: "{{project_path|quote}}"
|
||||
register: galaxy_result
|
||||
@@ -143,7 +147,10 @@
|
||||
register: doesCollectionRequirementsExist
|
||||
|
||||
- name: fetch galaxy collections from collections/requirements.yml
|
||||
command: ansible-galaxy collection install -r collections/requirements.yml -p {{collections_destination|quote}}{{ ' -' + 'v' * ansible_verbosity if ansible_verbosity else '' }}
|
||||
command: >
|
||||
ansible-galaxy collection install -r collections/requirements.yml
|
||||
--collections-path {{projects_root}}/.__awx_cache/{{local_path}}/stage/requirements_collections
|
||||
{{ ' -' + 'v' * ansible_verbosity if ansible_verbosity else '' }}
|
||||
args:
|
||||
chdir: "{{project_path|quote}}"
|
||||
register: galaxy_collection_result
|
||||
@@ -151,11 +158,11 @@
|
||||
changed_when: "'Installing ' in galaxy_collection_result.stdout"
|
||||
environment:
|
||||
ANSIBLE_FORCE_COLOR: false
|
||||
ANSIBLE_COLLECTIONS_PATHS: "{{ collections_destination }}"
|
||||
ANSIBLE_COLLECTIONS_PATHS: "{{projects_root}}/.__awx_cache/{{local_path}}/stage/requirements_collections"
|
||||
GIT_SSH_COMMAND: "ssh -o StrictHostKeyChecking=no"
|
||||
|
||||
when:
|
||||
- "ansible_version.full is version_compare('2.8', '>=')"
|
||||
- "ansible_version.full is version_compare('2.9', '>=')"
|
||||
- collections_enabled|bool
|
||||
tags:
|
||||
- install_collections
|
||||
|
||||
@@ -94,6 +94,7 @@ USE_TZ = True
|
||||
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, 'ui', 'static'),
|
||||
os.path.join(BASE_DIR, 'ui_next', 'build', 'static'),
|
||||
os.path.join(BASE_DIR, 'static'),
|
||||
)
|
||||
|
||||
@@ -162,13 +163,13 @@ ALLOWED_HOSTS = []
|
||||
REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST']
|
||||
|
||||
# If Tower is behind a reverse proxy/load balancer, use this setting to
|
||||
# whitelist the proxy IP addresses from which Tower should trust custom
|
||||
# allow the proxy IP addresses from which Tower should trust custom
|
||||
# REMOTE_HOST_HEADERS header values
|
||||
# REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', ''REMOTE_ADDR', 'REMOTE_HOST']
|
||||
# PROXY_IP_WHITELIST = ['10.0.1.100', '10.0.1.101']
|
||||
# PROXY_IP_ALLOWED_LIST = ['10.0.1.100', '10.0.1.101']
|
||||
# If this setting is an empty list (the default), the headers specified by
|
||||
# REMOTE_HOST_HEADERS will be trusted unconditionally')
|
||||
PROXY_IP_WHITELIST = []
|
||||
PROXY_IP_ALLOWED_LIST = []
|
||||
|
||||
CUSTOM_VENV_PATHS = []
|
||||
|
||||
@@ -253,6 +254,7 @@ TEMPLATES = [
|
||||
},
|
||||
'DIRS': [
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
os.path.join(BASE_DIR, 'ui_next', 'build'),
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -664,7 +666,7 @@ AD_HOC_COMMANDS = [
|
||||
'win_user',
|
||||
]
|
||||
|
||||
INV_ENV_VARIABLE_BLACKLIST = ("HOME", "USER", "_", "TERM")
|
||||
INV_ENV_VARIABLE_BLOCKED = ("HOME", "USER", "_", "TERM")
|
||||
|
||||
# ----------------
|
||||
# -- Amazon EC2 --
|
||||
@@ -692,11 +694,6 @@ EC2_REGION_NAMES = {
|
||||
'cn-north-1': _('China (Beijing)'),
|
||||
}
|
||||
|
||||
EC2_REGIONS_BLACKLIST = [
|
||||
'us-gov-west-1',
|
||||
'cn-north-1',
|
||||
]
|
||||
|
||||
# Inventory variable name/values for determining if host is active/enabled.
|
||||
EC2_ENABLED_VAR = 'ec2_state'
|
||||
EC2_ENABLED_VALUE = 'running'
|
||||
@@ -713,8 +710,6 @@ EC2_EXCLUDE_EMPTY_GROUPS = True
|
||||
# ------------
|
||||
# -- VMware --
|
||||
# ------------
|
||||
VMWARE_REGIONS_BLACKLIST = []
|
||||
|
||||
# Inventory variable name/values for determining whether a host is
|
||||
# active in vSphere.
|
||||
VMWARE_ENABLED_VAR = 'guest.gueststate'
|
||||
@@ -769,8 +764,6 @@ GCE_REGION_CHOICES = [
|
||||
('australia-southeast1-b', _('Australia Southeast (B)')),
|
||||
('australia-southeast1-c', _('Australia Southeast (C)')),
|
||||
]
|
||||
GCE_REGIONS_BLACKLIST = []
|
||||
|
||||
# Inventory variable name/value for determining whether a host is active
|
||||
# in Google Compute Engine.
|
||||
GCE_ENABLED_VAR = 'status'
|
||||
@@ -815,8 +808,6 @@ AZURE_RM_REGION_CHOICES = [
|
||||
('koreacentral', _('Korea Central')),
|
||||
('koreasouth', _('Korea South')),
|
||||
]
|
||||
AZURE_RM_REGIONS_BLACKLIST = []
|
||||
|
||||
AZURE_RM_GROUP_FILTER = r'^.+$'
|
||||
AZURE_RM_HOST_FILTER = r'^.+$'
|
||||
AZURE_RM_ENABLED_VAR = 'powerstate'
|
||||
|
||||
@@ -183,5 +183,4 @@ else:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
WEBSOCKET_ORIGIN_WHITELIST = ['https://localhost:8043', 'https://localhost:3000']
|
||||
AWX_CALLBACK_PROFILE = True
|
||||
|
||||
@@ -53,16 +53,19 @@ function AddEditCredentialsController (
|
||||
vm.form.disabled = !isEditable;
|
||||
}
|
||||
|
||||
vm.form.organization._disabled = !isOrgEditableByUser;
|
||||
vm.form._organization._disabled = !isOrgEditableByUser;
|
||||
// Only exists for permissions compatibility
|
||||
$scope.credential_obj = credential.get();
|
||||
|
||||
vm.form.organization._resource = 'organization';
|
||||
vm.form.organization._model = organization;
|
||||
vm.form.organization._route = 'credentials.edit.organization';
|
||||
vm.form.organization._value = credential.get('summary_fields.organization.id');
|
||||
vm.form.organization._displayValue = credential.get('summary_fields.organization.name');
|
||||
vm.form.organization._placeholder = strings.get('inputs.ORGANIZATION_PLACEHOLDER');
|
||||
// Custom credentials can have input fields named 'name', 'organization',
|
||||
// 'description', etc. Underscore these variables to make collisions
|
||||
// less likely to occur.
|
||||
vm.form._organization._resource = 'organization';
|
||||
vm.form._organization._model = organization;
|
||||
vm.form._organization._route = 'credentials.edit.organization';
|
||||
vm.form._organization._value = credential.get('summary_fields.organization.id');
|
||||
vm.form._organization._displayValue = credential.get('summary_fields.organization.name');
|
||||
vm.form._organization._placeholder = strings.get('inputs.ORGANIZATION_PLACEHOLDER');
|
||||
|
||||
vm.form.credential_type._resource = 'credential_type';
|
||||
vm.form.credential_type._model = credentialType;
|
||||
@@ -98,10 +101,10 @@ function AddEditCredentialsController (
|
||||
vm.form._formName = 'credential';
|
||||
vm.form.disabled = !credential.isCreatable();
|
||||
|
||||
vm.form.organization._resource = 'organization';
|
||||
vm.form.organization._route = 'credentials.add.organization';
|
||||
vm.form.organization._model = organization;
|
||||
vm.form.organization._placeholder = strings.get('inputs.ORGANIZATION_PLACEHOLDER');
|
||||
vm.form._organization._resource = 'organization';
|
||||
vm.form._organization._route = 'credentials.add.organization';
|
||||
vm.form._organization._model = organization;
|
||||
vm.form._organization._placeholder = strings.get('inputs.ORGANIZATION_PLACEHOLDER');
|
||||
|
||||
vm.form.credential_type._resource = 'credential_type';
|
||||
vm.form.credential_type._route = 'credentials.add.credentialType';
|
||||
@@ -112,7 +115,7 @@ function AddEditCredentialsController (
|
||||
|
||||
$scope.$watch('organization', () => {
|
||||
if ($scope.organization) {
|
||||
vm.form.organization._idFromModal = $scope.organization;
|
||||
vm.form._organization._idFromModal = $scope.organization;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
|
||||
<at-panel-body>
|
||||
<at-form state="vm.form" autocomplete="off" id="credential_form">
|
||||
<at-input-text col="4" tab="1" state="vm.form.name" id="credential_name_group"></at-input-text>
|
||||
<at-input-text col="4" tab="2" state="vm.form.description" id="credential_description_group"></at-input-text>
|
||||
<at-input-lookup col="4" tab="3" state="vm.form.organization" id="credential_organization_group"></at-input-lookup>
|
||||
<at-input-text col="4" tab="1" state="vm.form._name" id="credential_name_group"></at-input-text>
|
||||
<at-input-text col="4" tab="2" state="vm.form._description" id="credential_description_group"></at-input-text>
|
||||
<at-input-lookup col="4" tab="3" state="vm.form._organization" id="credential_organization_group"></at-input-lookup>
|
||||
|
||||
<at-divider></at-divider>
|
||||
|
||||
@@ -56,11 +56,11 @@
|
||||
on-item-select="vm.onInputSourceItemSelect"
|
||||
on-test="vm.onInputSourceTest"
|
||||
results-filter="vm.filterInputSourceCredentialResults"
|
||||
/>
|
||||
></at-input-source-lookup>
|
||||
<at-external-credential-test
|
||||
ng-if="vm.externalTest.metadataInputs"
|
||||
on-close="vm.onExternalTestClose"
|
||||
on-submit="vm.onExternalTest"
|
||||
form="vm.externalTest.form"
|
||||
/>
|
||||
></at-external-credential-test>
|
||||
<div ng-if="$state.current.name.includes('permissions.add')" ui-view="modal"></div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
ng-show="vm.selectedName"
|
||||
tag="vm.selectedName"
|
||||
icon="external"
|
||||
/>
|
||||
></at-tag>
|
||||
</div>
|
||||
<div
|
||||
class="InputSourceLookup-selectedItemText"
|
||||
@@ -45,7 +45,8 @@
|
||||
selected-id="vm.selectedId"
|
||||
on-ready="vm.onReady"
|
||||
on-item-select="vm.onItemSelect"
|
||||
/>
|
||||
>
|
||||
</at-lookup-list>
|
||||
<at-form state="vm.form" autocomplete="off" id="input_source_form">
|
||||
<at-input-group
|
||||
ng-if="vm.tabs.metadata._active"
|
||||
|
||||
@@ -12,7 +12,13 @@ function BaseInputController (strings) {
|
||||
|
||||
scope.state._touched = false;
|
||||
scope.state._required = scope.state.required || false;
|
||||
scope.state._isValid = scope.state._isValid || false;
|
||||
|
||||
if (scope.state.type === 'boolean') {
|
||||
scope.state._isValid = scope.state._isValid || true;
|
||||
} else {
|
||||
scope.state._isValid = scope.state._isValid || false;
|
||||
}
|
||||
|
||||
scope.state._disabled = scope.state._disabled || false;
|
||||
scope.state._activeModel = scope.state._activeModel || '_value';
|
||||
|
||||
@@ -59,6 +65,10 @@ function BaseInputController (strings) {
|
||||
scope.state._touched = true;
|
||||
}
|
||||
|
||||
if (scope.state.type === 'boolean') {
|
||||
return { isValid, message };
|
||||
}
|
||||
|
||||
if (scope.state._required && (!scope.state._value || !scope.state._value[0]) &&
|
||||
!scope.state._displayValue) {
|
||||
isValid = false;
|
||||
|
||||
@@ -22,12 +22,13 @@
|
||||
icon="external"
|
||||
tag="state._tagValue"
|
||||
remove-tag="state._onRemoveTag(state)"
|
||||
/>
|
||||
>
|
||||
</at-tag>
|
||||
<at-tag
|
||||
ng-show="state._disabled && state._tagValue"
|
||||
icon="external"
|
||||
tag="state._tagValue"
|
||||
/>
|
||||
></at-tag>
|
||||
</div>
|
||||
</span>
|
||||
<input ng-if="!state.asTag" type="text" class="form-control at-Input"
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
ng-class="{'at-InputFile--drag': drag }"
|
||||
type="file"
|
||||
name="files"
|
||||
/>
|
||||
></input>
|
||||
<div
|
||||
ng-if="state.asTag"
|
||||
ng-disabled="state._disabled || form.disabled"
|
||||
@@ -31,12 +31,12 @@
|
||||
icon="external"
|
||||
tag="state._tagValue"
|
||||
remove-tag="state._onRemoveTag(state)"
|
||||
/>
|
||||
></at-tag>
|
||||
<at-tag
|
||||
ng-show="state._disabled && state._tagValue"
|
||||
icon="external"
|
||||
tag="state._tagValue"
|
||||
/>
|
||||
></at-tag>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
@@ -49,7 +49,7 @@
|
||||
ng-attr-tabindex="{{ tab || undefined }}"
|
||||
ng-attr-placeholder="{{state._placeholder || undefined }}"
|
||||
ng-disabled="state._disabled || form.disabled"
|
||||
/>
|
||||
></textarea>
|
||||
<div ng-if="state._edit" class="input-group-btn at-InputGroup-button input-group-append">
|
||||
<button
|
||||
aria-label="{{:: vm.strings.get('secret.REPLACE')}}"
|
||||
|
||||
@@ -67,6 +67,10 @@
|
||||
&--external:before {
|
||||
content: '\f14c'
|
||||
}
|
||||
|
||||
&--kubernetes:before, &--kubernetes_bearer_token:before {
|
||||
content: '\f0c2';
|
||||
}
|
||||
}
|
||||
|
||||
.TagComponent-button {
|
||||
|
||||
@@ -27,6 +27,16 @@ function createFormSchema (method, config) {
|
||||
}
|
||||
});
|
||||
|
||||
// Custom credentials can have input fields named 'name', 'organization',
|
||||
// 'description', etc. Underscore these variables to make collisions
|
||||
// less likely to occur.
|
||||
schema._name = schema.name;
|
||||
schema._organization = schema.organization;
|
||||
schema._description = schema.description;
|
||||
delete schema.name;
|
||||
delete schema.organization;
|
||||
delete schema.description;
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export default ['Rest', 'Wait',
|
||||
data = credential_typeData,
|
||||
id = credential_typeData.id,
|
||||
form = CredentialTypesForm,
|
||||
master = {},
|
||||
main = {},
|
||||
url = GetBasePath('credential_types');
|
||||
|
||||
init();
|
||||
@@ -123,7 +123,7 @@ export default ['Rest', 'Wait',
|
||||
for (fld in form.fields) {
|
||||
if (data[fld] && fld !== 'inputs' || fld !== 'injectors') {
|
||||
$scope[fld] = data[fld];
|
||||
master[fld] = data[fld];
|
||||
main[fld] = data[fld];
|
||||
}
|
||||
|
||||
if (fld === "kind") {
|
||||
@@ -131,7 +131,7 @@ export default ['Rest', 'Wait',
|
||||
for (i = 0; i < $scope.credential_kind_options.length; i++) {
|
||||
if ($scope.kind === $scope.credential_kind_options[i].value) {
|
||||
$scope.kind = $scope.credential_kind_options[i];
|
||||
master[fld] = $scope.credential_kind_options[i];
|
||||
main[fld] = $scope.credential_kind_options[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
description: $scope.description,
|
||||
enabled: $scope.host.enabled
|
||||
};
|
||||
if (typeof $scope.host.instance_id !== 'undefined') {
|
||||
host.instance_id = $scope.host.instance_id;
|
||||
}
|
||||
HostsService.put(host).then(function(){
|
||||
$state.go('.', null, {reload: true});
|
||||
});
|
||||
|
||||
@@ -215,7 +215,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){
|
||||
dataTitle: i18n._("Source Variables"),
|
||||
dataPlacement: 'right',
|
||||
awPopOver: "<p>" + i18n._("Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables ") +
|
||||
"<a href=\"https://github.com/ansible-collections/community.aws/blob/master/scripts/inventory/ec2.ini\" target=\"_blank\">" +
|
||||
"<a href=\"https://github.com/ansible-collections/community.aws/blob/main/scripts/inventory/ec2.ini\" target=\"_blank\">" +
|
||||
i18n._("view ec2.ini in the community.aws repo.") + "</a></p>" +
|
||||
"<p>" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "</p>" +
|
||||
i18n._("JSON:") + "<br />\n" +
|
||||
@@ -239,7 +239,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){
|
||||
dataTitle: i18n._("Source Variables"),
|
||||
dataPlacement: 'right',
|
||||
awPopOver: "<p>" + i18n._("Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables ") +
|
||||
"<a href=\"https://github.com/ansible-collections/vmware/blob/master/scripts/inventory/vmware_inventory.ini\" target=\"_blank\">" +
|
||||
"<a href=\"https://github.com/ansible-collections/vmware/blob/main/scripts/inventory/vmware_inventory.ini\" target=\"_blank\">" +
|
||||
i18n._("view vmware_inventory.ini in the vmware community repo.") + "</a></p>" +
|
||||
"<p>" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "</p>" +
|
||||
i18n._("JSON:") + "<br />\n" +
|
||||
@@ -262,9 +262,9 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){
|
||||
parseTypeName: 'envParseType',
|
||||
dataTitle: i18n._("Source Variables"),
|
||||
dataPlacement: 'right',
|
||||
awPopOver: i18n._(`Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration
|
||||
<a href=\"https://github.com/openstack/ansible-collections-openstack/blob/master/scripts/inventory/openstack.yml\" target=\"_blank\">
|
||||
view openstack.yml in the Openstack github repo.</a> Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.`),
|
||||
awPopOver: i18n._("Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration") +
|
||||
'<a href=\"https://github.com/openstack/ansible-collections-openstack/blob/master/scripts/inventory/openstack.yml\" target=\"_blank\">' +
|
||||
i18n._("view openstack.yml in the Openstack github repo.") + "</a>" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax."),
|
||||
dataContainer: 'body',
|
||||
subForm: 'sourceSubForm'
|
||||
},
|
||||
@@ -279,9 +279,9 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){
|
||||
parseTypeName: 'envParseType',
|
||||
dataTitle: i18n._("Source Variables"),
|
||||
dataPlacement: 'right',
|
||||
awPopOver: i18n._(`Override variables found in cloudforms.ini and used by the inventory update script. For an example variable configuration
|
||||
<a href=\"https://github.com/ansible-collections/community.general/blob/master/scripts/inventory/cloudforms.ini\" target=\"_blank\">
|
||||
view cloudforms.ini in the Ansible Collections github repo.</a> Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.`),
|
||||
awPopOver: i18n._("Override variables found in cloudforms.ini and used by the inventory update script. For an example variable configuration") +
|
||||
'<a href=\"https://github.com/ansible-collections/community.general/blob/main/scripts/inventory/cloudforms.ini\" target=\"_blank\">' +
|
||||
i18n._("view cloudforms.ini in the Ansible Collections github repo.") + "</a>" + i18n._(" Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax."),
|
||||
dataContainer: 'body',
|
||||
subForm: 'sourceSubForm'
|
||||
},
|
||||
@@ -296,9 +296,9 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){
|
||||
parseTypeName: 'envParseType',
|
||||
dataTitle: i18n._("Source Variables"),
|
||||
dataPlacement: 'right',
|
||||
awPopOver: i18n._(`Override variables found in foreman.ini and used by the inventory update script. For an example variable configuration
|
||||
<a href=\"https://github.com/ansible-collections/community.general/blob/master/scripts/inventory/foreman.ini\" target=\"_blank\">
|
||||
view foreman.ini in the Ansible Collections github repo.</a> Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.`),
|
||||
awPopOver: i18n._("Override variables found in foreman.ini and used by the inventory update script. For an example variable configuration") +
|
||||
'<a href=\"https://github.com/ansible-collections/community.general/blob/main/scripts/inventory/foreman.ini\" target=\"_blank\">' +
|
||||
i18n._("view foreman.ini in the Ansible Collections github repo.") + "</a>" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax."),
|
||||
dataContainer: 'body',
|
||||
subForm: 'sourceSubForm'
|
||||
},
|
||||
@@ -314,7 +314,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){
|
||||
dataTitle: i18n._("Source Variables"),
|
||||
dataPlacement: 'right',
|
||||
awPopOver: "<p>" + i18n._("Override variables found in azure_rm.ini and used by the inventory update script. For a detailed description of these variables ") +
|
||||
"<a href=\"https://github.com/ansible-collections/community.general/blob/master/scripts/inventory/azure_rm.ini\" target=\"_blank\">" +
|
||||
"<a href=\"https://github.com/ansible-collections/community.general/blob/main/scripts/inventory/azure_rm.ini\" target=\"_blank\">" +
|
||||
i18n._("view azure_rm.ini in the Ansible community.general github repo.") + "</a></p>" +
|
||||
"<p>" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "</p>" +
|
||||
i18n._("JSON:") + "<br />\n" +
|
||||
|
||||
@@ -17,7 +17,7 @@ export default ['Rest', 'Wait',
|
||||
data = inventory_scriptData,
|
||||
id = inventory_scriptData.id,
|
||||
form = InventoryScriptsForm,
|
||||
master = {},
|
||||
main = {},
|
||||
url = GetBasePath('inventory_scripts');
|
||||
|
||||
init();
|
||||
@@ -35,14 +35,14 @@ export default ['Rest', 'Wait',
|
||||
for (fld in form.fields) {
|
||||
if (data[fld]) {
|
||||
$scope[fld] = data[fld];
|
||||
master[fld] = data[fld];
|
||||
main[fld] = data[fld];
|
||||
}
|
||||
|
||||
if (form.fields[fld].sourceModel && data.summary_fields &&
|
||||
data.summary_fields[form.fields[fld].sourceModel]) {
|
||||
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||
master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
main[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ export default ['$log', '$cookies', '$rootScope', 'ProcessErrors',
|
||||
}
|
||||
scope.customLoginInfo = $AnsibleConfig.custom_login_info;
|
||||
scope.customLoginInfoPresent = (scope.customLoginInfo) ? true : false;
|
||||
scope.customLoginInfoIsHTML = /<\/?[a-z][\s\S]*>/i.test(scope.customLoginInfo);
|
||||
});
|
||||
|
||||
if (scope.removeAuthorizationGetLicense) {
|
||||
|
||||
@@ -98,7 +98,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="login_modal_notice" class="LoginModalNotice" ng-if="customLoginInfoPresent"><div class="LoginModalNotice-title" translate>NOTICE</div>{{ customLoginInfo | sanitize }}</div>
|
||||
<div id="login_modal_notice" class="LoginModalNotice" ng-if="customLoginInfoPresent">
|
||||
<div class="LoginModalNotice-title" translate>NOTICE</div>
|
||||
<ng-bind-html ng-bind-html="customLoginInfo"
|
||||
ng-style="{'white-space' : customLoginInfoIsHTML ? 'initial' : 'pre-wrap'}">
|
||||
</ng-bind-html>
|
||||
</div>
|
||||
</div>
|
||||
<div class="LoginModal-footer">
|
||||
<div class="LoginModal-footerBlock">
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
color: @login-notice-text;
|
||||
overflow-y: scroll;
|
||||
overflow-x: visible;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.LoginModalNotice-title {
|
||||
|
||||
@@ -23,7 +23,7 @@ export default ['Rest', 'Wait',
|
||||
var generator = GenerateForm,
|
||||
id = notification_template.id,
|
||||
form = NotificationsFormObject,
|
||||
master = {},
|
||||
main = {},
|
||||
url = GetBasePath('notification_templates'),
|
||||
defaultMessages = {};
|
||||
|
||||
@@ -73,25 +73,25 @@ export default ['Rest', 'Wait',
|
||||
for (fld in form.fields) {
|
||||
if (data[fld]) {
|
||||
$scope[fld] = data[fld];
|
||||
master[fld] = data[fld];
|
||||
main[fld] = data[fld];
|
||||
}
|
||||
|
||||
if(data.notification_configuration.use_ssl === true){
|
||||
$scope.email_options = "use_ssl";
|
||||
master.email_options = "use_ssl";
|
||||
main.email_options = "use_ssl";
|
||||
$scope.use_ssl = true;
|
||||
master.use_ssl = true;
|
||||
main.use_ssl = true;
|
||||
$scope.use_tls = false;
|
||||
master.use_tls = false;
|
||||
main.use_tls = false;
|
||||
}
|
||||
|
||||
if(data.notification_configuration.use_tls === true){
|
||||
$scope.email_options = "use_tls";
|
||||
master.email_options = "use_tls";
|
||||
main.email_options = "use_tls";
|
||||
$scope.use_ssl = false;
|
||||
master.use_ssl = false;
|
||||
main.use_ssl = false;
|
||||
$scope.use_tls = true;
|
||||
master.use_tls = true;
|
||||
main.use_tls = true;
|
||||
}
|
||||
|
||||
if (data.notification_configuration.timeout === null ||
|
||||
@@ -101,7 +101,7 @@ export default ['Rest', 'Wait',
|
||||
|
||||
if (data.notification_configuration[fld]) {
|
||||
$scope[fld] = data.notification_configuration[fld];
|
||||
master[fld] = data.notification_configuration[fld];
|
||||
main[fld] = data.notification_configuration[fld];
|
||||
|
||||
if (form.fields[fld].type === 'textarea') {
|
||||
if (form.fields[fld].name === 'headers') {
|
||||
@@ -116,7 +116,7 @@ export default ['Rest', 'Wait',
|
||||
data.summary_fields[form.fields[fld].sourceModel]) {
|
||||
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||
master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
main[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ export default ['Rest', 'Wait',
|
||||
}
|
||||
}
|
||||
|
||||
master.notification_type = $scope.notification_type;
|
||||
main.notification_type = $scope.notification_type;
|
||||
CreateSelect2({
|
||||
element: '#notification_template_notification_type',
|
||||
multiple: false
|
||||
|
||||
@@ -14,7 +14,7 @@ export default ['$scope', '$location', '$stateParams', 'isOrgAdmin', 'isNotifica
|
||||
let form = OrganizationForm(),
|
||||
defaultUrl = GetBasePath('organizations'),
|
||||
base = $location.path().replace(/^\//, '').split('/')[0],
|
||||
master = {},
|
||||
main = {},
|
||||
id = $stateParams.organization_id,
|
||||
instance_group_url = defaultUrl + id + '/instance_groups/';
|
||||
|
||||
@@ -55,7 +55,7 @@ export default ['$scope', '$location', '$stateParams', 'isOrgAdmin', 'isNotifica
|
||||
for (fld in form.fields) {
|
||||
if (typeof data[fld] !== 'undefined') {
|
||||
$scope[fld] = data[fld];
|
||||
master[fld] = data[fld];
|
||||
main[fld] = data[fld];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ export default ['$scope', '$location', '$stateParams', 'isOrgAdmin', 'isNotifica
|
||||
});
|
||||
});
|
||||
$scope.organization_name = $scope.name;
|
||||
master = params;
|
||||
main = params;
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, OrganizationForm, {
|
||||
@@ -166,4 +166,4 @@ export default ['$scope', '$location', '$stateParams', 'isOrgAdmin', 'isNotifica
|
||||
|
||||
};
|
||||
}
|
||||
];
|
||||
];
|
||||
|
||||
@@ -15,7 +15,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
||||
let form = ProjectsForm(),
|
||||
base = $location.path().replace(/^\//, '').split('/')[0],
|
||||
defaultUrl = GetBasePath('projects'),
|
||||
master = {};
|
||||
main = {};
|
||||
|
||||
init();
|
||||
|
||||
@@ -46,7 +46,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
||||
GenerateForm.applyDefaults(form, $scope);
|
||||
}
|
||||
|
||||
GetProjectPath({ scope: $scope, master: master });
|
||||
GetProjectPath({ scope: $scope, main: main });
|
||||
|
||||
if ($scope.removeChoicesReady) {
|
||||
$scope.removeChoicesReady();
|
||||
@@ -67,7 +67,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
||||
});
|
||||
|
||||
$scope.scmRequired = false;
|
||||
master.scm_type = $scope.scm_type;
|
||||
main.scm_type = $scope.scm_type;
|
||||
});
|
||||
|
||||
// Load the list of options for Kind
|
||||
|
||||
@@ -17,7 +17,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest',
|
||||
|
||||
let form = ProjectsForm(),
|
||||
defaultUrl = GetBasePath('projects') + $stateParams.project_id + '/',
|
||||
master = {},
|
||||
main = {},
|
||||
id = $stateParams.project_id;
|
||||
|
||||
$scope.project_local_paths = [];
|
||||
@@ -46,7 +46,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest',
|
||||
$scope.projectLoadedRemove();
|
||||
}
|
||||
$scope.projectLoadedRemove = $scope.$on('projectLoaded', function() {
|
||||
GetProjectPath({ scope: $scope, master: master });
|
||||
GetProjectPath({ scope: $scope, main: main });
|
||||
|
||||
$scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false;
|
||||
$scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false;
|
||||
@@ -76,19 +76,19 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest',
|
||||
if (form.fields[fld].type === 'checkbox_group') {
|
||||
for (i = 0; i < form.fields[fld].fields.length; i++) {
|
||||
$scope[form.fields[fld].fields[i].name] = data[form.fields[fld].fields[i].name];
|
||||
master[form.fields[fld].fields[i].name] = data[form.fields[fld].fields[i].name];
|
||||
main[form.fields[fld].fields[i].name] = data[form.fields[fld].fields[i].name];
|
||||
}
|
||||
} else {
|
||||
if (data[fld] !== undefined) {
|
||||
$scope[fld] = data[fld];
|
||||
master[fld] = data[fld];
|
||||
main[fld] = data[fld];
|
||||
}
|
||||
}
|
||||
if (form.fields[fld].sourceModel && data.summary_fields &&
|
||||
data.summary_fields[form.fields[fld].sourceModel]) {
|
||||
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||
master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
main[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest',
|
||||
$scope.scmRequired = false;
|
||||
}
|
||||
|
||||
master.scm_type = $scope.scm_type;
|
||||
main.scm_type = $scope.scm_type;
|
||||
CreateSelect2({
|
||||
element: '#project_scm_type',
|
||||
multiple: false
|
||||
|
||||
@@ -2,7 +2,7 @@ export default
|
||||
function GetProjectPath(i18n, Rest, GetBasePath, ProcessErrors) {
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
master = params.master;
|
||||
main = params.main;
|
||||
|
||||
function arraySort(data) {
|
||||
//Sort nodes by name
|
||||
@@ -54,8 +54,8 @@ export default
|
||||
}
|
||||
}
|
||||
scope.base_dir = data.project_base_dir || i18n._('You do not have access to view this property');
|
||||
master.local_path = scope.local_path;
|
||||
master.base_dir = scope.base_dir; // Keep in master object so that it doesn't get
|
||||
main.local_path = scope.local_path;
|
||||
main.base_dir = scope.base_dir; // Keep in main object so that it doesn't get
|
||||
// wiped out on form reset.
|
||||
if (opts.length === 0) {
|
||||
// trigger display of alert block when scm_type == manual
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
type="text"
|
||||
readonly
|
||||
ng-model="dateValue"
|
||||
ng-class="inputClass()">
|
||||
ng-class="inputClass()"/>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
id="schedulerName"
|
||||
ng-model="schedulerName" required
|
||||
ng-disabled="!(schedule_obj.summary_fields.user_capabilities.edit || canAdd) || credentialRequiresPassword"
|
||||
placeholder="{{strings.get('form.SCHEDULE_NAME')}}">
|
||||
placeholder="{{strings.get('form.SCHEDULE_NAME')}}" />
|
||||
<div class="error"
|
||||
ng-show="scheduler_form.$dirty && scheduler_form.schedulerName.$error.required">
|
||||
{{ strings.get('form.NAME_REQUIRED_MESSAGE') }}
|
||||
@@ -74,7 +74,7 @@
|
||||
placeholder="{{strings.get('form.HH24')}}"
|
||||
aw-min="0" min="0" aw-max="23"
|
||||
max="23" data-zero-pad="2" required
|
||||
ng-change="timeChange()" >
|
||||
ng-change="timeChange()" />
|
||||
<span
|
||||
class="SchedulerTime-separator">
|
||||
:
|
||||
@@ -90,7 +90,7 @@
|
||||
placeholder="{{strings.get('form.MM')}}"
|
||||
min="0" max="59" data-zero-pad="2"
|
||||
required
|
||||
ng-change="timeChange()" >
|
||||
ng-change="timeChange()" />
|
||||
<span
|
||||
class="SchedulerTime-separator">
|
||||
:
|
||||
@@ -106,7 +106,7 @@
|
||||
placeholder="{{strings.get('form.SS')}}"
|
||||
min="0" max="59" data-zero-pad="2"
|
||||
required
|
||||
ng-change="timeChange()" >
|
||||
ng-change="timeChange()" />
|
||||
</div>
|
||||
<div class="error"
|
||||
ng-show="scheduler_startTime_error">
|
||||
@@ -173,7 +173,7 @@
|
||||
min="1"
|
||||
max="999"
|
||||
ng-change="resetError('scheduler_interval_error')"
|
||||
>
|
||||
/>
|
||||
<label class="inline-label
|
||||
RepeatFrequencyOptions-inlineLabel"
|
||||
ng-bind="schedulerIntervalLabel">
|
||||
@@ -196,7 +196,7 @@
|
||||
ng-model="$parent.monthlyRepeatOption"
|
||||
ng-change="monthlyRepeatChange()"
|
||||
name="monthlyRepeatOption"
|
||||
id="monthlyRepeatOption">
|
||||
id="monthlyRepeatOption" />
|
||||
{{ strings.get('form.ON_DAY') }}
|
||||
</label>
|
||||
</div>
|
||||
@@ -209,7 +209,7 @@
|
||||
aw-spinner="$parent.monthDay"
|
||||
ng-model="$parent.monthDay"
|
||||
min="1" max="31"
|
||||
ng-change="resetError('scheduler_monthDay_error')" >
|
||||
ng-change="resetError('scheduler_monthDay_error')" />
|
||||
<div class="error"
|
||||
ng-show="$parent.scheduler_monthDay_error">
|
||||
{{ strings.get('form.MONTH_DAY_ERROR_MESSAGE') }}
|
||||
@@ -228,7 +228,7 @@
|
||||
ng-model="$parent.monthlyRepeatOption"
|
||||
ng-change="monthlyRepeatChange()"
|
||||
name="monthlyRepeatOption"
|
||||
id="monthlyRepeatOption">
|
||||
id="monthlyRepeatOption" />
|
||||
{{ strings.get('form.ON_THE') }}
|
||||
</label>
|
||||
</div>
|
||||
@@ -267,7 +267,7 @@
|
||||
ng-model="$parent.yearlyRepeatOption"
|
||||
ng-change="yearlyRepeatChange()"
|
||||
name="yearlyRepeatOption"
|
||||
id="yearlyRepeatOption">
|
||||
id="yearlyRepeatOption" />
|
||||
{{ strings.get('form.ON') }}
|
||||
</label>
|
||||
</div>
|
||||
@@ -292,7 +292,7 @@
|
||||
ng-model="$parent.yearlyMonthDay"
|
||||
min="1" max="31"
|
||||
ng-change="resetError('scheduler_yearlyMonthDay_error')"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
<div class="error"
|
||||
ng-show="$parent.scheduler_yearlyMonthDay_error">
|
||||
@@ -312,7 +312,7 @@
|
||||
ng-model="$parent.yearlyRepeatOption"
|
||||
ng-change="yearlyRepeatChange()"
|
||||
name="yearlyRepeatOption"
|
||||
id="yearlyRepeatOption">
|
||||
id="yearlyRepeatOption" />
|
||||
{{ strings.get('form.ON_THE') }}
|
||||
</label>
|
||||
</div>
|
||||
@@ -524,7 +524,7 @@
|
||||
placeholder="{{strings.get('form.HH24')}}"
|
||||
aw-min="0" min="0" aw-max="23"
|
||||
max="23" data-zero-pad="2" required
|
||||
ng-change="schedulerEndChange('schedulerEndHour', $parent.schedulerEndHour)" >
|
||||
ng-change="schedulerEndChange('schedulerEndHour', $parent.schedulerEndHour)" />
|
||||
<span
|
||||
class="SchedulerTime-separator">
|
||||
:
|
||||
@@ -540,7 +540,7 @@
|
||||
placeholder="{{strings.get('form.MM')}}"
|
||||
min="0" max="59" data-zero-pad="2"
|
||||
required
|
||||
ng-change="schedulerEndChange('schedulerEndMinute', $parent.schedulerEndMinute)" >
|
||||
ng-change="schedulerEndChange('schedulerEndMinute', $parent.schedulerEndMinute)" />
|
||||
<span
|
||||
class="SchedulerTime-separator">
|
||||
:
|
||||
@@ -556,7 +556,7 @@
|
||||
placeholder="{{strings.get('form.SS')}}"
|
||||
min="0" max="59" data-zero-pad="2"
|
||||
required
|
||||
ng-change="schedulerEndChange('schedulerEndSecond', $parent.schedulerEndSecond)" >
|
||||
ng-change="schedulerEndChange('schedulerEndSecond', $parent.schedulerEndSecond)" />
|
||||
</div>
|
||||
<div class="error"
|
||||
ng-show="scheduler_startTime_error">
|
||||
@@ -604,7 +604,7 @@
|
||||
class="SchedulerFormDetail-radioButton"
|
||||
ng-model="dateChoice"
|
||||
id="date-choice-local"
|
||||
value="local" >
|
||||
value="local" />
|
||||
{{ strings.get('form.LOCAL_TIME_ZONE') }}
|
||||
</label>
|
||||
<label class="radio-inline
|
||||
@@ -613,7 +613,7 @@
|
||||
class="SchedulerFormDetail-radioButton"
|
||||
ng-model="dateChoice"
|
||||
id="date-choice-utc"
|
||||
value="utc" >
|
||||
value="utc" />
|
||||
UTC
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
let defaultUrl = GetBasePath('job_templates'),
|
||||
form = JobTemplateForm(),
|
||||
generator = GenerateForm,
|
||||
master = {},
|
||||
main = {},
|
||||
selectPlaybook, checkSCMStatus,
|
||||
callback;
|
||||
|
||||
@@ -157,14 +157,14 @@
|
||||
|
||||
hashSetup({
|
||||
scope: $scope,
|
||||
master: master,
|
||||
main: main,
|
||||
check_field: 'allow_callbacks',
|
||||
default_val: false
|
||||
});
|
||||
CallbackHelpInit({ scope: $scope });
|
||||
// set initial vals for webhook checkbox
|
||||
$scope.enable_webhook = false;
|
||||
master.enable_webhook = false;
|
||||
main.enable_webhook = false;
|
||||
|
||||
$scope.surveyTooltip = i18n._('Please save before adding a survey to this job template.');
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export default
|
||||
let defaultUrl = GetBasePath('job_templates'),
|
||||
generator = GenerateForm,
|
||||
form = JobTemplateForm(),
|
||||
master = {},
|
||||
main = {},
|
||||
id = $stateParams.job_template_id,
|
||||
callback,
|
||||
choicesCount = 0,
|
||||
@@ -470,15 +470,15 @@ export default
|
||||
if ($scope.jobTemplateLoadedRemove) {
|
||||
$scope.jobTemplateLoadedRemove();
|
||||
}
|
||||
$scope.jobTemplateLoadedRemove = $scope.$on('jobTemplateLoaded', function (e, masterObject) {
|
||||
$scope.jobTemplateLoadedRemove = $scope.$on('jobTemplateLoaded', function (e, mainObject) {
|
||||
var dft;
|
||||
|
||||
master = masterObject;
|
||||
main = mainObject;
|
||||
|
||||
dft = ($scope.host_config_key === "" || $scope.host_config_key === null) ? false : true;
|
||||
hashSetup({
|
||||
scope: $scope,
|
||||
master: master,
|
||||
main: main,
|
||||
check_field: 'allow_callbacks',
|
||||
default_val: dft
|
||||
});
|
||||
@@ -486,10 +486,10 @@ export default
|
||||
// set initial vals for webhook checkbox
|
||||
if (jobTemplateData.webhook_service) {
|
||||
$scope.enable_webhook = true;
|
||||
master.enable_webhook = true;
|
||||
main.enable_webhook = true;
|
||||
} else {
|
||||
$scope.enable_webhook = false;
|
||||
master.enable_webhook = false;
|
||||
main.enable_webhook = false;
|
||||
}
|
||||
|
||||
ParseTypeChange({
|
||||
@@ -540,17 +540,17 @@ export default
|
||||
$scope.survey_exists = true;
|
||||
}
|
||||
}
|
||||
master[fld] = $scope[fld];
|
||||
main[fld] = $scope[fld];
|
||||
}
|
||||
if (fld === 'extra_vars') {
|
||||
// Parse extra_vars, converting to YAML.
|
||||
$scope.extra_vars = ParseVariableString(jobTemplateData.extra_vars);
|
||||
master.extra_vars = $scope.extra_vars;
|
||||
main.extra_vars = $scope.extra_vars;
|
||||
}
|
||||
if (form.fields[fld].type === 'lookup' && jobTemplateData.summary_fields[form.fields[fld].sourceModel]) {
|
||||
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
jobTemplateData.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||
master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
main[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField];
|
||||
}
|
||||
if (form.fields[fld].type === 'checkbox_group') {
|
||||
@@ -565,44 +565,44 @@ export default
|
||||
$scope.survey_enabled = jobTemplateData.survey_enabled;
|
||||
|
||||
$scope.ask_variables_on_launch = (jobTemplateData.ask_variables_on_launch) ? true : false;
|
||||
master.ask_variables_on_launch = $scope.ask_variables_on_launch;
|
||||
main.ask_variables_on_launch = $scope.ask_variables_on_launch;
|
||||
|
||||
$scope.ask_verbosity_on_launch = (jobTemplateData.ask_verbosity_on_launch) ? true : false;
|
||||
master.ask_verbosity_on_launch = $scope.ask_verbosity_on_launch;
|
||||
main.ask_verbosity_on_launch = $scope.ask_verbosity_on_launch;
|
||||
|
||||
$scope.ask_limit_on_launch = (jobTemplateData.ask_limit_on_launch) ? true : false;
|
||||
master.ask_limit_on_launch = $scope.ask_limit_on_launch;
|
||||
main.ask_limit_on_launch = $scope.ask_limit_on_launch;
|
||||
|
||||
$scope.ask_tags_on_launch = (jobTemplateData.ask_tags_on_launch) ? true : false;
|
||||
master.ask_tags_on_launch = $scope.ask_tags_on_launch;
|
||||
main.ask_tags_on_launch = $scope.ask_tags_on_launch;
|
||||
|
||||
$scope.ask_skip_tags_on_launch = (jobTemplateData.ask_skip_tags_on_launch) ? true : false;
|
||||
master.ask_skip_tags_on_launch = $scope.ask_skip_tags_on_launch;
|
||||
main.ask_skip_tags_on_launch = $scope.ask_skip_tags_on_launch;
|
||||
|
||||
$scope.ask_diff_mode_on_launch = (jobTemplateData.ask_diff_mode_on_launch) ? true : false;
|
||||
master.ask_diff_mode_on_launch = $scope.ask_diff_mode_on_launch;
|
||||
main.ask_diff_mode_on_launch = $scope.ask_diff_mode_on_launch;
|
||||
|
||||
$scope.ask_scm_branch_on_launch = (jobTemplateData.ask_scm_branch_on_launch) ? true : false;
|
||||
master.ask_scm_branch_on_launch = $scope.ask_scm_branch_on_launch;
|
||||
main.ask_scm_branch_on_launch = $scope.ask_scm_branch_on_launch;
|
||||
|
||||
$scope.job_tag_options = (jobTemplateData.job_tags) ? jobTemplateData.job_tags.split(',')
|
||||
.map((i) => ({name: i, label: i, value: i})) : [];
|
||||
$scope.job_tags = $scope.job_tag_options;
|
||||
master.job_tags = $scope.job_tags;
|
||||
main.job_tags = $scope.job_tags;
|
||||
|
||||
$scope.skip_tag_options = (jobTemplateData.skip_tags) ? jobTemplateData.skip_tags.split(',')
|
||||
.map((i) => ({name: i, label: i, value: i})) : [];
|
||||
$scope.skip_tags = $scope.skip_tag_options;
|
||||
master.skip_tags = $scope.skip_tags;
|
||||
main.skip_tags = $scope.skip_tags;
|
||||
|
||||
$scope.ask_job_type_on_launch = (jobTemplateData.ask_job_type_on_launch) ? true : false;
|
||||
master.ask_job_type_on_launch = $scope.ask_job_type_on_launch;
|
||||
main.ask_job_type_on_launch = $scope.ask_job_type_on_launch;
|
||||
|
||||
$scope.ask_inventory_on_launch = (jobTemplateData.ask_inventory_on_launch) ? true : false;
|
||||
master.ask_inventory_on_launch = $scope.ask_inventory_on_launch;
|
||||
main.ask_inventory_on_launch = $scope.ask_inventory_on_launch;
|
||||
|
||||
$scope.ask_credential_on_launch = (jobTemplateData.ask_credential_on_launch) ? true : false;
|
||||
master.ask_credential_on_launch = $scope.ask_credential_on_launch;
|
||||
main.ask_credential_on_launch = $scope.ask_credential_on_launch;
|
||||
|
||||
if (jobTemplateData.host_config_key) {
|
||||
$scope.example_config_key = jobTemplateData.host_config_key;
|
||||
@@ -612,7 +612,7 @@ export default
|
||||
|
||||
$scope.callback_url = $scope.callback_server_path + ((jobTemplateData.related.callback) ? jobTemplateData.related.callback :
|
||||
GetBasePath('job_templates') + id + '/callback/');
|
||||
master.callback_url = $scope.callback_url;
|
||||
main.callback_url = $scope.callback_url;
|
||||
|
||||
$scope.can_edit = jobTemplateData.summary_fields.user_capabilities.edit;
|
||||
|
||||
@@ -645,7 +645,7 @@ export default
|
||||
$q.all(multiCredentialPromises)
|
||||
.then(() => {
|
||||
$scope.multiCredential = multiCredential;
|
||||
$scope.$emit('jobTemplateLoaded', master);
|
||||
$scope.$emit('jobTemplateLoaded', main);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ export default
|
||||
function hashSetup() {
|
||||
return function(params) {
|
||||
var scope = params.scope,
|
||||
master = params.master,
|
||||
main = params.main,
|
||||
check_field = params.check_field,
|
||||
default_val = params.default_val;
|
||||
|
||||
scope[check_field] = default_val;
|
||||
master[check_field] = default_val;
|
||||
main[check_field] = default_val;
|
||||
|
||||
// Original gist here: https://gist.github.com/jed/982883
|
||||
scope.genHash = function (fld) {
|
||||
|
||||
@@ -111,7 +111,7 @@ function multiCredentialModalController(GetBasePath, qs, MultiCredentialService)
|
||||
|
||||
scope.credentialTypes.forEach((credentialType => {
|
||||
if(credentialType.kind
|
||||
.match(/^(machine|cloud|net|ssh|vault)$/)) {
|
||||
.match(/^(machine|cloud|net|ssh|vault|kubernetes)$/)) {
|
||||
scope.displayedCredentialTypes.push(credentialType);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<i class="fa fa-code-fork MultiCredential-tagIcon" ng-switch-when="scm"></i>
|
||||
<i class="fa fa-key MultiCredential-tagIcon" ng-switch-when="ssh"></i>
|
||||
<i class="fa fa-archive MultiCredential-tagIcon" ng-switch-when="vault"></i>
|
||||
<i class="fa fa-cloud MultiCredential-tagIcon" ng-switch-when="kubernetes"></i>
|
||||
</div>
|
||||
<div class="MultiCredential-tag MultiCredential-tag--deletable">
|
||||
<span ng-if="!tag.info" class="MultiCredential-name--label ng-binding">
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<i class="fa fa-code-fork MultiCredential-tagIcon" ng-switch-when="scm"></i>
|
||||
<i class="fa fa-key MultiCredential-tagIcon" ng-switch-when="ssh"></i>
|
||||
<i class="fa fa-archive MultiCredential-tagIcon" ng-switch-when="vault"></i>
|
||||
<i class="fa fa-cloud MultiCredential-tagIcon" ng-switch-when="kubernetes"></i>
|
||||
</div>
|
||||
<div class="MultiCredential-iconContainer" ng-switch="tag.kind" ng-if="!fieldIsDisabled">
|
||||
<i class="fa fa-cloud MultiCredential-tagIcon" ng-switch-when="cloud"></i>
|
||||
@@ -34,6 +35,7 @@
|
||||
<i class="fa fa-code-fork MultiCredential-tagIcon" ng-switch-when="scm"></i>
|
||||
<i class="fa fa-key MultiCredential-tagIcon" ng-switch-when="ssh"></i>
|
||||
<i class="fa fa-archive MultiCredential-tagIcon" ng-switch-when="vault"></i>
|
||||
<i class="fa fa-cloud MultiCredential-tagIcon" ng-switch-when="kubernetes"></i>
|
||||
</div>
|
||||
<div class="MultiCredential-tag"
|
||||
ng-class="{'MultiCredential-tag--deletable': !fieldIsDisabled, 'MultiCredential-tag--disabled': fieldIsDisabled}">
|
||||
|
||||
@@ -55,7 +55,7 @@ export default [ 'ProcessErrors', 'CredentialTypeModel', 'TemplatesStrings', '$f
|
||||
vm.promptDataClone.prompts.credentials.credentialTypeOptions = [];
|
||||
response.data.results.forEach((credentialTypeRow => {
|
||||
vm.promptDataClone.prompts.credentials.credentialTypes[credentialTypeRow.id] = credentialTypeRow.kind;
|
||||
if(credentialTypeRow.kind.match(/^(cloud|net|ssh|vault)$/)) {
|
||||
if(credentialTypeRow.kind.match(/^(cloud|net|ssh|vault|kubernetes)$/)) {
|
||||
if(credentialTypeRow.kind === 'ssh') {
|
||||
vm.promptDataClone.prompts.credentials.credentialKind = credentialTypeRow.id.toString();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest',
|
||||
|
||||
const { me } = models;
|
||||
var form = UserForm,
|
||||
master = {},
|
||||
main = {},
|
||||
id = $stateParams.user_id,
|
||||
defaultUrl = GetBasePath('users') + id,
|
||||
user_obj = resourceData.data;
|
||||
@@ -50,7 +50,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest',
|
||||
$scope.user_id = id;
|
||||
$scope.ldap_user = (user_obj.ldap_dn !== null && user_obj.ldap_dn !== undefined && user_obj.ldap_dn !== '') ? true : false;
|
||||
$scope.not_ldap_user = !$scope.ldap_user;
|
||||
master.ldap_user = $scope.ldap_user;
|
||||
main.ldap_user = $scope.ldap_user;
|
||||
$scope.socialAuthUser = (user_obj.auth.length > 0) ? true : false;
|
||||
$scope.last_login = user_obj.last_login;
|
||||
$scope.external_account = user_obj.external_account;
|
||||
|
||||
@@ -31,8 +31,8 @@ register(
|
||||
label=_('Custom Login Info'),
|
||||
help_text=_('If needed, you can add specific information (such as a legal '
|
||||
'notice or a disclaimer) to a text box in the login modal using '
|
||||
'this setting. Any content added must be in plain text, as '
|
||||
'custom HTML or other markup languages are not supported.'),
|
||||
'this setting. Any content added must be in plain text or an '
|
||||
'HTML fragment, as other markup languages are not supported.'),
|
||||
category=_('UI'),
|
||||
category_slug='ui',
|
||||
)
|
||||
|
||||
60
awx/ui/package-lock.json
generated
60
awx/ui/package-lock.json
generated
@@ -241,11 +241,11 @@
|
||||
"integrity": "sha512-nB/xe7JQWF9nLvhHommAICQ3eWrfRETo0EVGFESi952CDzDa+GAJ/2BFBNw44QqQPxj1Xua/uYKrbLsOGWZdbQ=="
|
||||
},
|
||||
"angular-scheduler": {
|
||||
"version": "git+https://git@github.com/ansible/angular-scheduler.git#6a2d33b06b1143e7449c4427f222fd05559f3a23",
|
||||
"from": "git+https://git@github.com/ansible/angular-scheduler.git#v0.4.3",
|
||||
"version": "git+https://git@github.com/ansible/angular-scheduler.git#d72b62f47fb5c11b3284eaaea11c4d5525fa3b99",
|
||||
"from": "git+https://git@github.com/ansible/angular-scheduler.git#v0.4.4",
|
||||
"requires": {
|
||||
"angular": "^1.7.9",
|
||||
"angular-tz-extensions": "github:ansible/angular-tz-extensions#5c594b5756d29637601020bba16274f10ee0ed65",
|
||||
"angular-tz-extensions": "github:ansible/angular-tz-extensions",
|
||||
"jquery": "^3.5.1",
|
||||
"jquery-ui": "*",
|
||||
"lodash": "^4.17.15",
|
||||
@@ -261,14 +261,9 @@
|
||||
"angular-filters": "^1.1.2",
|
||||
"jquery": "^3.5.1",
|
||||
"jstimezonedetect": "1.0.5",
|
||||
"timezone-js": "github:ansible/timezone-js#6937de14ce0c193961538bb5b3b12b7ef62a358f"
|
||||
"timezone-js": "github:ansible/timezone-js#0.4.14"
|
||||
}
|
||||
},
|
||||
"jquery": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
|
||||
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
|
||||
},
|
||||
"rrule": {
|
||||
"version": "github:jkbrzt/rrule#4ff63b2f8524fd6d5ba6e80db770953b5cd08a0c",
|
||||
"from": "github:jkbrzt/rrule#4ff63b2f8524fd6d5ba6e80db770953b5cd08a0c"
|
||||
@@ -283,13 +278,17 @@
|
||||
"angular-filters": "^1.1.2",
|
||||
"jquery": "^3.5.1",
|
||||
"jstimezonedetect": "1.0.5",
|
||||
"timezone-js": "github:ansible/timezone-js#6937de14ce0c193961538bb5b3b12b7ef62a358f"
|
||||
"timezone-js": "github:ansible/timezone-js#0.4.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"jquery": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
|
||||
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
|
||||
},
|
||||
"timezone-js": {
|
||||
"version": "github:ansible/timezone-js#6937de14ce0c193961538bb5b3b12b7ef62a358f",
|
||||
"from": "github:ansible/timezone-js#0.4.14"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1803,6 +1802,7 @@
|
||||
"resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
|
||||
"integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"hoek": "2.x.x"
|
||||
}
|
||||
@@ -5374,7 +5374,8 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@@ -5398,13 +5399,15 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -5414,19 +5417,22 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@@ -5547,7 +5553,8 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@@ -5561,6 +5568,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -5577,6 +5585,7 @@
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@@ -5682,7 +5691,8 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
|
||||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -5696,6 +5706,7 @@
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -5791,7 +5802,8 @@
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@@ -5833,6 +5845,7 @@
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -5854,6 +5867,7 @@
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@@ -5886,7 +5900,8 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6633,7 +6648,8 @@
|
||||
"version": "2.16.3",
|
||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
|
||||
"integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"home-or-tmp": {
|
||||
"version": "2.0.0",
|
||||
@@ -9131,6 +9147,7 @@
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
|
||||
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@@ -9140,7 +9157,8 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
|
||||
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
"angular-moment": "^1.3.0",
|
||||
"angular-mousewheel": "^1.0.5",
|
||||
"angular-sanitize": "^1.7.9",
|
||||
"angular-scheduler": "git+https://git@github.com/ansible/angular-scheduler.git#v0.4.3",
|
||||
"angular-scheduler": "git+https://git@github.com/ansible/angular-scheduler.git#v0.4.4",
|
||||
"angular-tz-extensions": "git+https://git@github.com/ansible/angular-tz-extensions.git#v0.6.1",
|
||||
"angular-xeditable": "~0.8.0",
|
||||
"ansi-to-html": "^0.6.3",
|
||||
|
||||
@@ -10,7 +10,7 @@ const initializeData = () => {
|
||||
data.ORGANIZATION_NAME = `organization-${id}`;
|
||||
data.PROJECT_NAME = `project-${id}`;
|
||||
data.PROJECT_URL = 'https://github.com/ansible/test-playbooks';
|
||||
data.PROJECT_BRANCH = 'master';
|
||||
data.PROJECT_BRANCH = 'devel';
|
||||
data.PLAYBOOK_NAME = 'multivault.yml';
|
||||
data.TEMPLATE_NAME = `template-${id}`;
|
||||
data.VAULT_CREDENTIAL_NAME_1 = `credential-vault-${id}-1`;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"localeDir": "src/locales/",
|
||||
"srcPathDirs": ["src/"],
|
||||
"format": "po"
|
||||
"format": "po",
|
||||
"sourceLocale": "en"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user