mirror of
https://github.com/ansible/awx.git
synced 2026-02-05 03:24:50 -03:30
Compare commits
735 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d7420317b | ||
|
|
6bdb106128 | ||
|
|
a128a94842 | ||
|
|
f7a455bc83 | ||
|
|
f51377ff85 | ||
|
|
dc0862bbe7 | ||
|
|
5b1350db75 | ||
|
|
12f564e4a3 | ||
|
|
d32394f1b6 | ||
|
|
f5fee8e6e7 | ||
|
|
9eb7042d8c | ||
|
|
9b95cc27c4 | ||
|
|
2439aa409d | ||
|
|
cb60f12b6b | ||
|
|
41b0367627 | ||
|
|
c25dbb534f | ||
|
|
d0d08c2395 | ||
|
|
1242ee2b65 | ||
|
|
7d0f062d9e | ||
|
|
aea08eef6b | ||
|
|
aef263fa6c | ||
|
|
35d9a8f839 | ||
|
|
fef6e0b191 | ||
|
|
b620d8505a | ||
|
|
9fc1378fd1 | ||
|
|
b46db98b5a | ||
|
|
14bdf8deb3 | ||
|
|
24df1d7be6 | ||
|
|
40ead6f9d1 | ||
|
|
2a71232dd6 | ||
|
|
8b301f91ab | ||
|
|
a4b2d6bf88 | ||
|
|
b8b98b136b | ||
|
|
c4b4b319c9 | ||
|
|
dd8ca5acc4 | ||
|
|
172864a3a1 | ||
|
|
a691340986 | ||
|
|
261f1427e9 | ||
|
|
9383512772 | ||
|
|
2d81923e22 | ||
|
|
1093a662f1 | ||
|
|
210517eeb1 | ||
|
|
2ffe3d9a85 | ||
|
|
6737bd4c19 | ||
|
|
29ad847544 | ||
|
|
b3ef2c928a | ||
|
|
7936dff188 | ||
|
|
43c552c7c6 | ||
|
|
e0357d53f5 | ||
|
|
beb1dd5ae7 | ||
|
|
d464df557b | ||
|
|
5e9f790554 | ||
|
|
47b325896d | ||
|
|
c85d58e28d | ||
|
|
bcbb768dd3 | ||
|
|
e0693d3746 | ||
|
|
a6edc46cc3 | ||
|
|
f24b08316d | ||
|
|
25c14382db | ||
|
|
796d7bf67f | ||
|
|
ddef41d394 | ||
|
|
c626f51dae | ||
|
|
a9bb1eba02 | ||
|
|
b1c87c1793 | ||
|
|
612205d56d | ||
|
|
94b1455f40 | ||
|
|
b26bd11924 | ||
|
|
287159296a | ||
|
|
23100094dc | ||
|
|
b2275c0490 | ||
|
|
d6dba784b1 | ||
|
|
2a68ff49c1 | ||
|
|
0f8c59523a | ||
|
|
de02df4907 | ||
|
|
522dcf5ed3 | ||
|
|
b2d84a5d89 | ||
|
|
7b390fa2fc | ||
|
|
557ec27303 | ||
|
|
f47a37a96b | ||
|
|
74d8fca673 | ||
|
|
7039f82d15 | ||
|
|
e34833c8cb | ||
|
|
f22314caaf | ||
|
|
fb0c82598f | ||
|
|
90f7e9375f | ||
|
|
ccea920ea3 | ||
|
|
2d636806db | ||
|
|
bda42332b7 | ||
|
|
2ee03b552d | ||
|
|
7a5efa1adc | ||
|
|
2ff3b5d62c | ||
|
|
e23ee41082 | ||
|
|
2aa32f61f8 | ||
|
|
036e1ad82e | ||
|
|
66321a6218 | ||
|
|
1f31cc9394 | ||
|
|
758ad164fe | ||
|
|
be975fc051 | ||
|
|
04ea39c315 | ||
|
|
12e0c31fe6 | ||
|
|
869d259433 | ||
|
|
0dba3f53b1 | ||
|
|
73c87f9512 | ||
|
|
7faff07bd9 | ||
|
|
ca83b62c21 | ||
|
|
f77298643f | ||
|
|
8aa33b9b4a | ||
|
|
987cfed649 | ||
|
|
9b6644bc77 | ||
|
|
88a4362a7a | ||
|
|
0dcbafaccb | ||
|
|
3b17170533 | ||
|
|
7afaacb5e3 | ||
|
|
b06421b870 | ||
|
|
320581a6c0 | ||
|
|
d4f50896de | ||
|
|
71812c66d2 | ||
|
|
731d8c21e8 | ||
|
|
3247983823 | ||
|
|
485536d4cf | ||
|
|
b37040a85c | ||
|
|
b5b38c1b79 | ||
|
|
84ad1cdfcd | ||
|
|
75a72637dd | ||
|
|
74e4c17b63 | ||
|
|
c65ae87d69 | ||
|
|
7feb6515e1 | ||
|
|
d3b3b6e8f5 | ||
|
|
48e02f373f | ||
|
|
dbf8df479b | ||
|
|
764947c1ae | ||
|
|
0b724682da | ||
|
|
4fb055345d | ||
|
|
d3ed6ac73a | ||
|
|
d22cafc42e | ||
|
|
58444a75b9 | ||
|
|
7178c1d9e0 | ||
|
|
945d9156a6 | ||
|
|
bf86719412 | ||
|
|
12c0b80102 | ||
|
|
1d2c21249b | ||
|
|
3371a6f386 | ||
|
|
e79fbab737 | ||
|
|
8f74bad1c1 | ||
|
|
02d320de71 | ||
|
|
5cb8bd34ac | ||
|
|
b457926b38 | ||
|
|
55ce409a12 | ||
|
|
051bbcaeb5 | ||
|
|
2de5fbdac7 | ||
|
|
43592cbe00 | ||
|
|
0a6fc8cb89 | ||
|
|
5e6562023d | ||
|
|
14280ec53b | ||
|
|
edef496583 | ||
|
|
cfc0a4771f | ||
|
|
f6ddb72482 | ||
|
|
52851c57d8 | ||
|
|
a503529d05 | ||
|
|
a2a245c89e | ||
|
|
f3bf35311e | ||
|
|
67d619f9cc | ||
|
|
463357c81e | ||
|
|
7b3e5cd8d5 | ||
|
|
ec1fa4dae6 | ||
|
|
e49b9a202e | ||
|
|
059fa9001a | ||
|
|
cc36b46925 | ||
|
|
657a6f3a93 | ||
|
|
9dda5404a0 | ||
|
|
c3823771a7 | ||
|
|
69426dee08 | ||
|
|
22dbe5c0f9 | ||
|
|
d8452e1259 | ||
|
|
cb50cdce0d | ||
|
|
cd672baa13 | ||
|
|
30709d1ab2 | ||
|
|
f382fce576 | ||
|
|
36d2d03bc7 | ||
|
|
b21e491075 | ||
|
|
740402e5a8 | ||
|
|
5662d8b625 | ||
|
|
af6ea1cc58 | ||
|
|
f185d80b05 | ||
|
|
0a5f29ad22 | ||
|
|
3b7b27ea20 | ||
|
|
7fee9e35c4 | ||
|
|
f33b343cd8 | ||
|
|
012852ec53 | ||
|
|
ee56e9ccfb | ||
|
|
e3cb8d0447 | ||
|
|
0b10ff7fe6 | ||
|
|
c9a4cb7696 | ||
|
|
7a5cf4b81c | ||
|
|
4e45a3b365 | ||
|
|
096f5fb324 | ||
|
|
416d30a189 | ||
|
|
cda5cc25b8 | ||
|
|
508d8311dd | ||
|
|
54f9dd5e98 | ||
|
|
4fe558392a | ||
|
|
a98be1443b | ||
|
|
e35f7acd05 | ||
|
|
4aa4490933 | ||
|
|
e72f0bcfd4 | ||
|
|
534418c81a | ||
|
|
91b8aa90ff | ||
|
|
5874becb00 | ||
|
|
19b41743de | ||
|
|
ffcb655038 | ||
|
|
2ae93261d1 | ||
|
|
a8c51670af | ||
|
|
5df424803f | ||
|
|
241d7f57b7 | ||
|
|
9ba8feaec1 | ||
|
|
886d29e111 | ||
|
|
7452b82856 | ||
|
|
b7b17b9176 | ||
|
|
29e17ac49e | ||
|
|
189e12f8b3 | ||
|
|
cc2869d0c2 | ||
|
|
79d8b74221 | ||
|
|
8d8d9292bc | ||
|
|
7965f94027 | ||
|
|
efc45ac1fa | ||
|
|
6bfbcb35cd | ||
|
|
8cd05679c2 | ||
|
|
510d56b245 | ||
|
|
dc1bfaac3f | ||
|
|
92d8948a83 | ||
|
|
d3cc1a8771 | ||
|
|
5df2b1f346 | ||
|
|
7ff7517bdf | ||
|
|
957984d9e9 | ||
|
|
69a25bb092 | ||
|
|
b45f3f6cab | ||
|
|
0f02daa64d | ||
|
|
001fc1293c | ||
|
|
f4550900bb | ||
|
|
e2de8e4d5f | ||
|
|
07664a05fd | ||
|
|
4407aeac20 | ||
|
|
d59975c1ad | ||
|
|
cc24d524ac | ||
|
|
457c6287a2 | ||
|
|
3322123dd4 | ||
|
|
a53509b359 | ||
|
|
a87c6ddf1b | ||
|
|
0ea4a4dedd | ||
|
|
cc192246d9 | ||
|
|
0b3245eab6 | ||
|
|
b640203f88 | ||
|
|
82c7052d6f | ||
|
|
349a9c7cc2 | ||
|
|
e7ec1c6ef8 | ||
|
|
a011896cc0 | ||
|
|
a4899d4dbb | ||
|
|
1200c23ebc | ||
|
|
b8b2209335 | ||
|
|
500765cea5 | ||
|
|
f14934f42c | ||
|
|
a3fdb4aee3 | ||
|
|
fed24ed6df | ||
|
|
c9c7d2c2a5 | ||
|
|
cc0b2bb5b4 | ||
|
|
e29710ebab | ||
|
|
0ec274d13c | ||
|
|
0fc8179ca3 | ||
|
|
c6de6b8f25 | ||
|
|
516aecf7de | ||
|
|
eea3d72ffc | ||
|
|
1490235752 | ||
|
|
b136ce1e1d | ||
|
|
f704f320b5 | ||
|
|
c4a4275a89 | ||
|
|
64d4b71ec9 | ||
|
|
1be496cfc1 | ||
|
|
420b19cfb9 | ||
|
|
5287af1b9f | ||
|
|
f71421f60a | ||
|
|
f4da620c4d | ||
|
|
ffade973a9 | ||
|
|
1bae944b85 | ||
|
|
33f7bf67e1 | ||
|
|
6fe93f474f | ||
|
|
bdad9ac8f9 | ||
|
|
d74c3a09e5 | ||
|
|
ee5b4b072b | ||
|
|
9d66b583b7 | ||
|
|
3c06c97c32 | ||
|
|
8cfe74a854 | ||
|
|
e5dda696d7 | ||
|
|
82db7df6b3 | ||
|
|
f57876b6d9 | ||
|
|
38bb4f3f3c | ||
|
|
5ae7cbb43a | ||
|
|
1509ef3e80 | ||
|
|
df87681e6d | ||
|
|
621cc3f839 | ||
|
|
e8d73babaf | ||
|
|
9880f1e124 | ||
|
|
a1002b03fa | ||
|
|
47bdbddbeb | ||
|
|
f7bd9af7a1 | ||
|
|
261980f18e | ||
|
|
986641de9f | ||
|
|
6f789b661f | ||
|
|
667cbb0c20 | ||
|
|
ec3be57539 | ||
|
|
54499dbf69 | ||
|
|
a6f79c646d | ||
|
|
ce49cb9ba4 | ||
|
|
5030eb35b6 | ||
|
|
8fa9535b98 | ||
|
|
db4734be85 | ||
|
|
e1333f5e00 | ||
|
|
ae72d8dce5 | ||
|
|
2067718c0e | ||
|
|
25db22e072 | ||
|
|
ca6153c955 | ||
|
|
76a7a76e81 | ||
|
|
2daf202e52 | ||
|
|
178d519f6e | ||
|
|
a414c4e60e | ||
|
|
7c2554be8c | ||
|
|
468a290ba6 | ||
|
|
d6f91f8b2d | ||
|
|
53f564068f | ||
|
|
e11c2df6b6 | ||
|
|
14c1b85127 | ||
|
|
526b640329 | ||
|
|
63894bf822 | ||
|
|
b9e0b2e0ad | ||
|
|
83e6255ba4 | ||
|
|
09a950570e | ||
|
|
64aecb85fa | ||
|
|
85b9b4f896 | ||
|
|
a808462a3d | ||
|
|
b17fb8a596 | ||
|
|
bab7095d67 | ||
|
|
e36320174c | ||
|
|
abc3733449 | ||
|
|
344713f938 | ||
|
|
ad0e409448 | ||
|
|
81267c7212 | ||
|
|
fa232a94bd | ||
|
|
722ae932ab | ||
|
|
aea4a04c66 | ||
|
|
e20cf72dd6 | ||
|
|
af3419c2dd | ||
|
|
84f45d122d | ||
|
|
c4ffc58228 | ||
|
|
96906c2ece | ||
|
|
0f2355f416 | ||
|
|
731da8049b | ||
|
|
909c5e77c4 | ||
|
|
2ab688932d | ||
|
|
70137dea5a | ||
|
|
3d6790a419 | ||
|
|
212d3d517d | ||
|
|
09fd8e8106 | ||
|
|
c0b882d6fb | ||
|
|
a42a1bfa17 | ||
|
|
89ecddf662 | ||
|
|
de55ec1688 | ||
|
|
2062124a92 | ||
|
|
868ad51158 | ||
|
|
c2a223bbb4 | ||
|
|
268d50a339 | ||
|
|
04bd4d973a | ||
|
|
8700f32ffc | ||
|
|
9781c22c3f | ||
|
|
99b2350778 | ||
|
|
0c63a57418 | ||
|
|
3f2cc53992 | ||
|
|
20f27f4062 | ||
|
|
40b88da9dd | ||
|
|
2002d48bcc | ||
|
|
6353d5e410 | ||
|
|
cc0fd6beb6 | ||
|
|
f3a07753e6 | ||
|
|
64e933acb4 | ||
|
|
77ab6ec044 | ||
|
|
4e80f05cdf | ||
|
|
ae622c4875 | ||
|
|
40b2539626 | ||
|
|
a430b5bf9a | ||
|
|
aeed1d8ee9 | ||
|
|
5419434daa | ||
|
|
9b314a6f2f | ||
|
|
02cd188c2f | ||
|
|
f3a6da20f6 | ||
|
|
f0c94c7e9c | ||
|
|
1cb2a95a47 | ||
|
|
7e414ace5a | ||
|
|
e3a6a20049 | ||
|
|
7bd8234edf | ||
|
|
d30aed9231 | ||
|
|
c288c5fcbe | ||
|
|
fc80470e5d | ||
|
|
3596d776fc | ||
|
|
dbeef0a823 | ||
|
|
a4493cd02b | ||
|
|
a5683fb354 | ||
|
|
518ecee53e | ||
|
|
6bd5ee4201 | ||
|
|
390832cc1a | ||
|
|
c6d810621f | ||
|
|
0f6a40014e | ||
|
|
91e4679311 | ||
|
|
0495214f47 | ||
|
|
21156d1409 | ||
|
|
198dfe7f2e | ||
|
|
e1ebcd51b0 | ||
|
|
fe5821eb15 | ||
|
|
24f0fe2980 | ||
|
|
188eaede43 | ||
|
|
bbb31eb478 | ||
|
|
d96b88e495 | ||
|
|
601214f6d4 | ||
|
|
5b3f5206c4 | ||
|
|
ecb7306c46 | ||
|
|
3b65068258 | ||
|
|
a3bea6d4a8 | ||
|
|
4d15df2b48 | ||
|
|
e935776067 | ||
|
|
9f86fc2def | ||
|
|
35ecd83214 | ||
|
|
86a92fefe7 | ||
|
|
bf7e6201a2 | ||
|
|
a3a80bc23e | ||
|
|
5659270d3e | ||
|
|
6f26383e06 | ||
|
|
053b21e832 | ||
|
|
1443625d89 | ||
|
|
3cd54c45eb | ||
|
|
a7b51c526a | ||
|
|
ffefba9bf9 | ||
|
|
ff339e0eba | ||
|
|
c3fc00c45a | ||
|
|
d2cf2c275b | ||
|
|
f1fefbf5f0 | ||
|
|
b6eacbab86 | ||
|
|
92d3cb6dc4 | ||
|
|
de3cc4637e | ||
|
|
e28776962d | ||
|
|
87d9df5876 | ||
|
|
56bd145f21 | ||
|
|
581ec8860b | ||
|
|
1599d2c62c | ||
|
|
83982d5e2e | ||
|
|
8be8663665 | ||
|
|
740a9c1e61 | ||
|
|
9229c8e724 | ||
|
|
7567a6b36a | ||
|
|
240d07b6d4 | ||
|
|
09107aef1f | ||
|
|
68225d191a | ||
|
|
58f273347c | ||
|
|
c4065a54bd | ||
|
|
50ebf65178 | ||
|
|
f5f67627db | ||
|
|
e0df011804 | ||
|
|
f9d615fdee | ||
|
|
da8c3f6c43 | ||
|
|
c3493b0539 | ||
|
|
bba1c4f5b6 | ||
|
|
2a254ea538 | ||
|
|
b340d49cb7 | ||
|
|
91f87b6d81 | ||
|
|
7c009fc315 | ||
|
|
f09eb182c2 | ||
|
|
614116c90e | ||
|
|
b4007c7e04 | ||
|
|
1f07fc8494 | ||
|
|
2b18cee9c0 | ||
|
|
97477b789a | ||
|
|
f2ab7f62b9 | ||
|
|
35c94e9cd8 | ||
|
|
de658939c5 | ||
|
|
cbc1ae8875 | ||
|
|
c1381f7b98 | ||
|
|
6431ec603f | ||
|
|
fb7ccdb726 | ||
|
|
9619513017 | ||
|
|
c67088628f | ||
|
|
680d153a14 | ||
|
|
cdc8b372f9 | ||
|
|
0b7c643e75 | ||
|
|
8e194baa66 | ||
|
|
df30a2e8d1 | ||
|
|
241f8a8ac8 | ||
|
|
9d9b94c8c3 | ||
|
|
56c0ab97ed | ||
|
|
8846e1427e | ||
|
|
736f1e1775 | ||
|
|
21bdd487e6 | ||
|
|
4ce19a4b42 | ||
|
|
98cb8c6f6e | ||
|
|
a2007b8e0c | ||
|
|
0e9e17f957 | ||
|
|
6ed36daef7 | ||
|
|
5778c9cf05 | ||
|
|
2579e30ca1 | ||
|
|
701eb6afa5 | ||
|
|
e4d44efea2 | ||
|
|
3c0744629b | ||
|
|
990851aa3b | ||
|
|
65e369c0f3 | ||
|
|
9048c34a7d | ||
|
|
21298c8872 | ||
|
|
b820e411d3 | ||
|
|
dd522c240e | ||
|
|
54e79a93d9 | ||
|
|
02d7006ea4 | ||
|
|
7de89f6486 | ||
|
|
2588832629 | ||
|
|
f37bdba645 | ||
|
|
28b5d43e1f | ||
|
|
47719776f2 | ||
|
|
6c19d6ae4e | ||
|
|
c97dfeb725 | ||
|
|
2c19a5a1d7 | ||
|
|
4f929c7052 | ||
|
|
58f99c8918 | ||
|
|
3060abab1d | ||
|
|
a70a0fa622 | ||
|
|
7230b4bf8d | ||
|
|
fc32cf026f | ||
|
|
e8fe6fe33c | ||
|
|
215c23609c | ||
|
|
c5cd659c83 | ||
|
|
a1d1dc7a24 | ||
|
|
a1419f0f20 | ||
|
|
c085fc6751 | ||
|
|
57b1c2c42c | ||
|
|
395e30509b | ||
|
|
517ef8a2c9 | ||
|
|
d040f063e9 | ||
|
|
e77efbfec2 | ||
|
|
f34ec4be10 | ||
|
|
f521fe5cbc | ||
|
|
2d3152ef41 | ||
|
|
c63896fbb6 | ||
|
|
f76e9bddf9 | ||
|
|
83e300636d | ||
|
|
913077c489 | ||
|
|
e782be10b6 | ||
|
|
7406421d1b | ||
|
|
976c490dc3 | ||
|
|
a83e5e5675 | ||
|
|
8756da59fa | ||
|
|
4936238344 | ||
|
|
cb0367ac28 | ||
|
|
48e1fbfb38 | ||
|
|
bbd94fa4f7 | ||
|
|
164464c595 | ||
|
|
5bff942110 | ||
|
|
6a7ba87a02 | ||
|
|
3f2e47b1b1 | ||
|
|
df6877bb99 | ||
|
|
b69522b5aa | ||
|
|
95861491cb | ||
|
|
e07db0c05e | ||
|
|
8d62b7a2e3 | ||
|
|
f6f6643622 | ||
|
|
0d565eb3e3 | ||
|
|
23e34bcbbe | ||
|
|
87101a487d | ||
|
|
6446e45165 | ||
|
|
afcfd1640e | ||
|
|
e015558190 | ||
|
|
e5cdea8daf | ||
|
|
5948ecce16 | ||
|
|
24208197e8 | ||
|
|
dce50fe18b | ||
|
|
3e201d3ca0 | ||
|
|
4ccce4cc9e | ||
|
|
31d0347553 | ||
|
|
8f4437e17e | ||
|
|
a023df2c17 | ||
|
|
f2760ed91c | ||
|
|
9c6df68557 | ||
|
|
6efd523db2 | ||
|
|
f975f9fa75 | ||
|
|
a2601d5f67 | ||
|
|
18505b35b8 | ||
|
|
8bd85193ab | ||
|
|
70840841c1 | ||
|
|
1c483a42c6 | ||
|
|
2254bdb0e1 | ||
|
|
23bb32b7ad | ||
|
|
e8924e8f6f | ||
|
|
e77d81dd5b | ||
|
|
a217a387c6 | ||
|
|
13680a436c | ||
|
|
6c307726db | ||
|
|
9bc87b3e80 | ||
|
|
f678e158f8 | ||
|
|
11583dbff0 | ||
|
|
e48c734925 | ||
|
|
e25dcb2448 | ||
|
|
ebd09883fe | ||
|
|
fefbb8fff8 | ||
|
|
f5119e5d97 | ||
|
|
5fcdd16f54 | ||
|
|
e30b198418 | ||
|
|
aaa9096b4e | ||
|
|
4eb04b6f5c | ||
|
|
6dc11a926e | ||
|
|
7f1c3c8c6a | ||
|
|
fe857ad68b | ||
|
|
fd513f704b | ||
|
|
46e9fcfda7 | ||
|
|
32378266bd | ||
|
|
ff0015e21d | ||
|
|
6ce88fdf4d | ||
|
|
21cf1d85e3 | ||
|
|
d43f0cb2fc | ||
|
|
71ace1bc00 | ||
|
|
656e6d4f6a | ||
|
|
b375963165 | ||
|
|
b3b6e0515e | ||
|
|
b8fc402d55 | ||
|
|
e58613b441 | ||
|
|
14f1c4b652 | ||
|
|
1bb86dbdf0 | ||
|
|
3d730ef8d2 | ||
|
|
5d4aa56f4a | ||
|
|
9292b21a41 | ||
|
|
4a8791693f | ||
|
|
9114c16a97 | ||
|
|
b2ba863569 | ||
|
|
ef9f9e902e | ||
|
|
ac8553df85 | ||
|
|
6adcac85a6 | ||
|
|
7ea5ea2ecd | ||
|
|
44029c2191 | ||
|
|
9627a73978 | ||
|
|
9c7d449a4d | ||
|
|
d047bc876a | ||
|
|
27e13ca082 | ||
|
|
9400bad990 | ||
|
|
1dd8175e11 | ||
|
|
356ad06d74 | ||
|
|
e736cfab36 | ||
|
|
a31ef24be6 | ||
|
|
8f54ec681d | ||
|
|
aaaf598ca1 | ||
|
|
00c9ae1376 | ||
|
|
f83b59cb48 | ||
|
|
9341c4660c | ||
|
|
e5320b6fa6 | ||
|
|
27542ea322 | ||
|
|
3a8d95b03b | ||
|
|
aab6aa4ef9 | ||
|
|
12c8267b12 | ||
|
|
1e7ab9deed | ||
|
|
8f6b476388 | ||
|
|
58d6e586cd | ||
|
|
7d2bc1c766 | ||
|
|
07d2a1ed1e | ||
|
|
6d315568d2 | ||
|
|
7fdf27eece | ||
|
|
4e6e715f1f | ||
|
|
9979eddbcd | ||
|
|
de96f6cf8a | ||
|
|
9111948959 | ||
|
|
e3a5f32b57 | ||
|
|
546d5d5587 | ||
|
|
b0855ee33d | ||
|
|
03f6e52cf1 | ||
|
|
44e9d3919d | ||
|
|
f520be71d6 | ||
|
|
06470a0e65 | ||
|
|
5ea40efd3a | ||
|
|
4632383a33 | ||
|
|
ea0f3a64b1 | ||
|
|
df57b144c4 | ||
|
|
7b099578c8 | ||
|
|
08d2718f5e | ||
|
|
96b8ab47c4 | ||
|
|
07f6508402 | ||
|
|
a670a73fd0 | ||
|
|
90d1ab88b1 | ||
|
|
19dcf5ed59 | ||
|
|
90273247ac | ||
|
|
8a3b8823ee | ||
|
|
b40c81cc3d | ||
|
|
e53a6a91d6 | ||
|
|
55586b9b2a | ||
|
|
986d299961 | ||
|
|
7c97989e84 | ||
|
|
ecd8427a51 | ||
|
|
5c2e6244c6 | ||
|
|
fed7f51476 | ||
|
|
3ccfc5905e | ||
|
|
74d3e55908 | ||
|
|
557e619db6 | ||
|
|
2dfbae79bd | ||
|
|
fd28cff412 | ||
|
|
ca0127d889 | ||
|
|
05d72ae8cf | ||
|
|
1caa5b1c54 | ||
|
|
2245d6a22e | ||
|
|
3e9a85a58b | ||
|
|
51c58d5645 | ||
|
|
cfb89f1e31 | ||
|
|
fba1a5b71a | ||
|
|
79e68b1dbe | ||
|
|
3938d49a1f | ||
|
|
0373058540 | ||
|
|
22112f3dd8 | ||
|
|
a76ac805f2 | ||
|
|
311346b77b | ||
|
|
05af4c7c53 | ||
|
|
9f2b2b3456 | ||
|
|
0fbd0c941a | ||
|
|
a54fb0e27d | ||
|
|
ff53a9c8ea | ||
|
|
070cec19df | ||
|
|
f639f353ec | ||
|
|
ab94398889 | ||
|
|
13f6e63f75 | ||
|
|
b31edef9b2 | ||
|
|
b2ebbc6a0a | ||
|
|
d926378cf5 | ||
|
|
f8a4b01da5 | ||
|
|
0986ebef33 | ||
|
|
065813ebc0 | ||
|
|
a73c1dd28e | ||
|
|
421aa09383 | ||
|
|
72af9c1405 | ||
|
|
1bfa8b19ff | ||
|
|
43f3b484f9 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,6 +28,9 @@ awx/ui/build_test
|
||||
awx/ui/client/languages
|
||||
awx/ui/templates/ui/index.html
|
||||
awx/ui/templates/ui/installing.html
|
||||
awx/ui_next/node_modules/
|
||||
awx/ui_next/coverage/
|
||||
awx/ui_next/build/locales/_build
|
||||
/tower-license
|
||||
/tower-license/**
|
||||
tools/prometheus/data
|
||||
|
||||
20
Makefile
20
Makefile
@@ -73,6 +73,9 @@ clean-ui:
|
||||
rm -rf awx/ui/test/spec/reports/
|
||||
rm -rf awx/ui/test/e2e/reports/
|
||||
rm -rf awx/ui/client/languages/
|
||||
rm -rf awx/ui_next/node_modules/
|
||||
rm -rf awx/ui_next/coverage/
|
||||
rm -rf awx/ui_next/build/locales/_build/
|
||||
rm -f $(UI_DEPS_FLAG_FILE)
|
||||
rm -f $(UI_RELEASE_DEPS_FLAG_FILE)
|
||||
rm -f $(UI_RELEASE_FLAG_FILE)
|
||||
@@ -98,7 +101,7 @@ clean: clean-ui clean-dist
|
||||
rm -rf awx/job_status
|
||||
rm -rf awx/job_output
|
||||
rm -rf reports
|
||||
rm -f awx/awx_test.sqlite3
|
||||
rm -f awx/awx_test.sqlite3*
|
||||
rm -rf requirements/vendor
|
||||
rm -rf tmp
|
||||
rm -rf $(I18N_FLAG_FILE)
|
||||
@@ -515,6 +518,21 @@ jshint: $(UI_DEPS_FLAG_FILE)
|
||||
# END UI TASKS
|
||||
# --------------------------------------
|
||||
|
||||
# UI NEXT TASKS
|
||||
# --------------------------------------
|
||||
|
||||
ui-next-lint:
|
||||
$(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
|
||||
|
||||
# END UI NEXT TASKS
|
||||
# --------------------------------------
|
||||
|
||||
# Build a pip-installable package into dist/ with a timestamped version number.
|
||||
dev_build:
|
||||
$(PYTHON) setup.py dev_build
|
||||
|
||||
@@ -25,9 +25,8 @@ import hashlib
|
||||
|
||||
try:
|
||||
import django
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from django.db.backends.base import schema
|
||||
from django.db.backends.utils import names_digest
|
||||
HAS_DJANGO = True
|
||||
except ImportError:
|
||||
HAS_DJANGO = False
|
||||
@@ -37,30 +36,33 @@ if HAS_DJANGO is True:
|
||||
# This line exists to make sure we don't regress on FIPS support if we
|
||||
# upgrade Django; if you're upgrading Django and see this error,
|
||||
# update the version check below, and confirm that FIPS still works.
|
||||
if django.__version__ != '1.11.20':
|
||||
raise RuntimeError("Django version other than 1.11.20 detected {}. \
|
||||
Subclassing BaseDatabaseSchemaEditor is known to work for Django 1.11.20 \
|
||||
and may not work in newer Django versions.".format(django.__version__))
|
||||
# If operating in a FIPS environment, `hashlib.md5()` will raise a `ValueError`,
|
||||
# but will support the `usedforsecurity` keyword on RHEL and Centos systems.
|
||||
|
||||
# Keep an eye on https://code.djangoproject.com/ticket/28401
|
||||
target_version = '2.2.2'
|
||||
if django.__version__ != target_version:
|
||||
raise RuntimeError(
|
||||
"Django version other than {target} detected: {current}. "
|
||||
"Overriding `names_digest` is known to work for Django {target} "
|
||||
"and may not work in other Django versions.".format(target=target_version,
|
||||
current=django.__version__)
|
||||
)
|
||||
|
||||
class FipsBaseDatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||
|
||||
@classmethod
|
||||
def _digest(cls, *args):
|
||||
try:
|
||||
names_digest('foo', 'bar', 'baz', length=8)
|
||||
except ValueError:
|
||||
def names_digest(*args, length):
|
||||
"""
|
||||
Generates a 32-bit digest of a set of arguments that can be used to
|
||||
shorten identifying names.
|
||||
Generate a 32-bit digest of a set of arguments that can be used to shorten
|
||||
identifying names. Support for use in FIPS environments.
|
||||
"""
|
||||
try:
|
||||
h = hashlib.md5()
|
||||
except ValueError:
|
||||
h = hashlib.md5(usedforsecurity=False)
|
||||
h = hashlib.md5(usedforsecurity=False)
|
||||
for arg in args:
|
||||
h.update(force_bytes(arg))
|
||||
return h.hexdigest()[:8]
|
||||
h.update(arg.encode())
|
||||
return h.hexdigest()[:length]
|
||||
|
||||
|
||||
schema.BaseDatabaseSchemaEditor = FipsBaseDatabaseSchemaEditor
|
||||
schema.names_digest = names_digest
|
||||
|
||||
|
||||
def find_commands(management_dir):
|
||||
|
||||
@@ -401,21 +401,21 @@ class ListAPIView(generics.ListAPIView, GenericAPIView):
|
||||
continue
|
||||
if getattr(field, 'related_model', None):
|
||||
fields.add('{}__search'.format(field.name))
|
||||
for rel in self.model._meta.related_objects:
|
||||
name = rel.related_name
|
||||
if isinstance(rel, OneToOneRel) and self.model._meta.verbose_name.startswith('unified'):
|
||||
for related in self.model._meta.related_objects:
|
||||
name = related.related_name
|
||||
if isinstance(related, OneToOneRel) and self.model._meta.verbose_name.startswith('unified'):
|
||||
# Add underscores for polymorphic subclasses for user utility
|
||||
name = rel.related_model._meta.verbose_name.replace(" ", "_")
|
||||
name = related.related_model._meta.verbose_name.replace(" ", "_")
|
||||
if skip_related_name(name) or name.endswith('+'):
|
||||
continue
|
||||
fields.add('{}__search'.format(name))
|
||||
m2m_rel = []
|
||||
m2m_rel += self.model._meta.local_many_to_many
|
||||
m2m_related = []
|
||||
m2m_related += self.model._meta.local_many_to_many
|
||||
if issubclass(self.model, UnifiedJobTemplate) and self.model != UnifiedJobTemplate:
|
||||
m2m_rel += UnifiedJobTemplate._meta.local_many_to_many
|
||||
m2m_related += UnifiedJobTemplate._meta.local_many_to_many
|
||||
if issubclass(self.model, UnifiedJob) and self.model != UnifiedJob:
|
||||
m2m_rel += UnifiedJob._meta.local_many_to_many
|
||||
for relationship in m2m_rel:
|
||||
m2m_related += UnifiedJob._meta.local_many_to_many
|
||||
for relationship in m2m_related:
|
||||
if skip_related_name(relationship.name):
|
||||
continue
|
||||
if relationship.related_model._meta.app_label != 'main':
|
||||
|
||||
@@ -95,7 +95,7 @@ class ModelAccessPermission(permissions.BasePermission):
|
||||
'''
|
||||
|
||||
# Don't allow anonymous users. 401, not 403, hence no raised exception.
|
||||
if not request.user or request.user.is_anonymous():
|
||||
if not request.user or request.user.is_anonymous:
|
||||
return False
|
||||
|
||||
# Always allow superusers
|
||||
|
||||
@@ -2560,7 +2560,7 @@ class CredentialSerializer(BaseSerializer):
|
||||
|
||||
def validate_credential_type(self, credential_type):
|
||||
if self.instance and credential_type.pk != self.instance.credential_type.pk:
|
||||
for rel in (
|
||||
for related_objects in (
|
||||
'ad_hoc_commands',
|
||||
'insights_inventories',
|
||||
'unifiedjobs',
|
||||
@@ -2569,7 +2569,7 @@ class CredentialSerializer(BaseSerializer):
|
||||
'projectupdates',
|
||||
'workflowjobnodes'
|
||||
):
|
||||
if getattr(self.instance, rel).count() > 0:
|
||||
if getattr(self.instance, related_objects).count() > 0:
|
||||
raise ValidationError(
|
||||
_('You cannot change the credential type of the credential, as it may break the functionality'
|
||||
' of the resources using it.'),
|
||||
@@ -4640,37 +4640,37 @@ class ActivityStreamSerializer(BaseSerializer):
|
||||
return ""
|
||||
|
||||
def get_related(self, obj):
|
||||
rel = {}
|
||||
data = {}
|
||||
if obj.actor is not None:
|
||||
rel['actor'] = self.reverse('api:user_detail', kwargs={'pk': obj.actor.pk})
|
||||
data['actor'] = self.reverse('api:user_detail', kwargs={'pk': obj.actor.pk})
|
||||
for fk, __ in self._local_summarizable_fk_fields:
|
||||
if not hasattr(obj, fk):
|
||||
continue
|
||||
m2m_list = self._get_rel(obj, fk)
|
||||
m2m_list = self._get_related_objects(obj, fk)
|
||||
if m2m_list:
|
||||
rel[fk] = []
|
||||
data[fk] = []
|
||||
id_list = []
|
||||
for thisItem in m2m_list:
|
||||
if getattr(thisItem, 'id', None) in id_list:
|
||||
for item in m2m_list:
|
||||
if getattr(item, 'id', None) in id_list:
|
||||
continue
|
||||
id_list.append(getattr(thisItem, 'id', None))
|
||||
if hasattr(thisItem, 'get_absolute_url'):
|
||||
rel_url = thisItem.get_absolute_url(self.context.get('request'))
|
||||
id_list.append(getattr(item, 'id', None))
|
||||
if hasattr(item, 'get_absolute_url'):
|
||||
url = item.get_absolute_url(self.context.get('request'))
|
||||
else:
|
||||
view_name = fk + '_detail'
|
||||
rel_url = self.reverse('api:' + view_name, kwargs={'pk': thisItem.id})
|
||||
rel[fk].append(rel_url)
|
||||
url = self.reverse('api:' + view_name, kwargs={'pk': item.id})
|
||||
data[fk].append(url)
|
||||
|
||||
if fk == 'schedule':
|
||||
rel['unified_job_template'] = thisItem.unified_job_template.get_absolute_url(self.context.get('request'))
|
||||
data['unified_job_template'] = item.unified_job_template.get_absolute_url(self.context.get('request'))
|
||||
if obj.setting and obj.setting.get('category', None):
|
||||
rel['setting'] = self.reverse(
|
||||
data['setting'] = self.reverse(
|
||||
'api:setting_singleton_detail',
|
||||
kwargs={'category_slug': obj.setting['category']}
|
||||
)
|
||||
return rel
|
||||
return data
|
||||
|
||||
def _get_rel(self, obj, fk):
|
||||
def _get_related_objects(self, obj, fk):
|
||||
related_model = ActivityStream._meta.get_field(fk).related_model
|
||||
related_manager = getattr(obj, fk)
|
||||
if issubclass(related_model, PolymorphicModel) and hasattr(obj, '_prefetched_objects_cache'):
|
||||
@@ -4680,43 +4680,34 @@ class ActivityStreamSerializer(BaseSerializer):
|
||||
obj._prefetched_objects_cache[related_manager.prefetch_cache_name] = list(related_manager.all())
|
||||
return related_manager.all()
|
||||
|
||||
def _summarize_parent_ujt(self, obj, fk, summary_fields):
|
||||
summary_keys = {'job': 'job_template',
|
||||
'workflow_job_template_node': 'workflow_job_template',
|
||||
'schedule': 'unified_job_template'}
|
||||
if fk not in summary_keys:
|
||||
return
|
||||
related_obj = getattr(obj, summary_keys[fk], None)
|
||||
item = {}
|
||||
fields = SUMMARIZABLE_FK_FIELDS[summary_keys[fk]]
|
||||
if related_obj is not None:
|
||||
summary_fields[get_type_for_model(related_obj)] = []
|
||||
for field in fields:
|
||||
fval = getattr(related_obj, field, None)
|
||||
if fval is not None:
|
||||
item[field] = fval
|
||||
summary_fields[get_type_for_model(related_obj)].append(item)
|
||||
|
||||
def get_summary_fields(self, obj):
|
||||
summary_fields = OrderedDict()
|
||||
for fk, related_fields in self._local_summarizable_fk_fields:
|
||||
try:
|
||||
if not hasattr(obj, fk):
|
||||
continue
|
||||
m2m_list = self._get_rel(obj, fk)
|
||||
m2m_list = self._get_related_objects(obj, fk)
|
||||
if m2m_list:
|
||||
summary_fields[fk] = []
|
||||
for thisItem in m2m_list:
|
||||
if fk == 'job':
|
||||
summary_fields['job_template'] = []
|
||||
job_template_item = {}
|
||||
job_template_fields = SUMMARIZABLE_FK_FIELDS['job_template']
|
||||
job_template = getattr(thisItem, 'job_template', None)
|
||||
if job_template is not None:
|
||||
for field in job_template_fields:
|
||||
fval = getattr(job_template, field, None)
|
||||
if fval is not None:
|
||||
job_template_item[field] = fval
|
||||
summary_fields['job_template'].append(job_template_item)
|
||||
if fk == 'workflow_job_template_node':
|
||||
summary_fields['workflow_job_template'] = []
|
||||
workflow_job_template_item = {}
|
||||
workflow_job_template_fields = SUMMARIZABLE_FK_FIELDS['workflow_job_template']
|
||||
workflow_job_template = getattr(thisItem, 'workflow_job_template', None)
|
||||
if workflow_job_template is not None:
|
||||
for field in workflow_job_template_fields:
|
||||
fval = getattr(workflow_job_template, field, None)
|
||||
if fval is not None:
|
||||
workflow_job_template_item[field] = fval
|
||||
summary_fields['workflow_job_template'].append(workflow_job_template_item)
|
||||
if fk == 'schedule':
|
||||
unified_job_template = getattr(thisItem, 'unified_job_template', None)
|
||||
if unified_job_template is not None:
|
||||
summary_fields[get_type_for_model(unified_job_template)] = {'id': unified_job_template.id,
|
||||
'name': unified_job_template.name}
|
||||
self._summarize_parent_ujt(thisItem, fk, summary_fields)
|
||||
thisItemDict = {}
|
||||
for field in related_fields:
|
||||
fval = getattr(thisItem, field, None)
|
||||
|
||||
@@ -2997,7 +2997,7 @@ class WorkflowJobTemplateNodeChildrenBaseList(EnforceParentRelationshipMixin, Su
|
||||
relationships = ['success_nodes', 'failure_nodes', 'always_nodes']
|
||||
relationships.remove(self.relationship)
|
||||
qs = functools.reduce(lambda x, y: (x | y),
|
||||
(Q(**{'{}__in'.format(rel): [sub.id]}) for rel in relationships))
|
||||
(Q(**{'{}__in'.format(r): [sub.id]}) for r in relationships))
|
||||
|
||||
if models.WorkflowJobTemplateNode.objects.filter(Q(pk=parent.id) & qs).exists():
|
||||
return {"Error": _("Relationship not allowed.")}
|
||||
|
||||
@@ -21,7 +21,8 @@ class Migration(migrations.Migration):
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('key', models.CharField(max_length=255)),
|
||||
('value', jsonfield.fields.JSONField(null=True)),
|
||||
('user', models.ForeignKey(related_name='settings', default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('user', models.ForeignKey(related_name='settings', default=None, editable=False,
|
||||
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
|
||||
@@ -8,6 +8,7 @@ from awx.main.utils.encryption import decrypt_field
|
||||
from awx.conf import fields
|
||||
from awx.conf.registry import settings_registry
|
||||
from awx.conf.models import Setting
|
||||
from awx.sso import fields as sso_fields
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -137,7 +138,7 @@ def test_setting_signleton_retrieve_hierachy(api_request, dummy_setting):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_setting_signleton_retrieve_readonly(api_request, dummy_setting):
|
||||
def test_setting_singleton_retrieve_readonly(api_request, dummy_setting):
|
||||
with dummy_setting(
|
||||
'FOO_BAR',
|
||||
field_class=fields.IntegerField,
|
||||
@@ -183,6 +184,30 @@ def test_setting_singleton_update(api_request, dummy_setting):
|
||||
assert response.data['FOO_BAR'] == 4
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_setting_singleton_update_hybriddictfield_with_forbidden(api_request, dummy_setting):
|
||||
# Some HybridDictField subclasses have a child of _Forbidden,
|
||||
# indicating that only the defined fields can be filled in. Make
|
||||
# sure that the _Forbidden validator doesn't get used for the
|
||||
# fields. See also https://github.com/ansible/awx/issues/4099.
|
||||
with dummy_setting(
|
||||
'FOO_BAR',
|
||||
field_class=sso_fields.SAMLOrgAttrField,
|
||||
category='FooBar',
|
||||
category_slug='foobar',
|
||||
), mock.patch('awx.conf.views.handle_setting_changes'):
|
||||
api_request(
|
||||
'patch',
|
||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}),
|
||||
data={'FOO_BAR': {'saml_admin_attr': 'Admins', 'saml_attr': 'Orgs'}}
|
||||
)
|
||||
response = api_request(
|
||||
'get',
|
||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})
|
||||
)
|
||||
assert response.data['FOO_BAR'] == {'saml_admin_attr': 'Admins', 'saml_attr': 'Orgs'}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_setting_singleton_update_dont_change_readonly_fields(api_request, dummy_setting):
|
||||
with dummy_setting(
|
||||
@@ -206,7 +231,7 @@ def test_setting_singleton_update_dont_change_readonly_fields(api_request, dummy
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_setting_singleton_update_dont_change_encripted_mark(api_request, dummy_setting):
|
||||
def test_setting_singleton_update_dont_change_encrypted_mark(api_request, dummy_setting):
|
||||
with dummy_setting(
|
||||
'FOO_BAR',
|
||||
field_class=fields.CharField,
|
||||
|
||||
@@ -24,7 +24,7 @@ def ws_connect(message):
|
||||
headers = dict(message.content.get('headers', ''))
|
||||
message.reply_channel.send({"accept": True})
|
||||
message.content['method'] = 'FAKE'
|
||||
if message.user.is_authenticated():
|
||||
if message.user.is_authenticated:
|
||||
message.reply_channel.send(
|
||||
{"text": json.dumps({"accept": True, "user": message.user.id})}
|
||||
)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from .plugin import CredentialPlugin
|
||||
|
||||
import os
|
||||
import stat
|
||||
import tempfile
|
||||
import threading
|
||||
from urllib.parse import quote, urlencode, urljoin
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import requests
|
||||
|
||||
# AWX
|
||||
from awx.main.utils import (
|
||||
create_temporary_fifo,
|
||||
)
|
||||
|
||||
aim_inputs = {
|
||||
'fields': [{
|
||||
@@ -60,24 +60,6 @@ aim_inputs = {
|
||||
}
|
||||
|
||||
|
||||
def create_temporary_fifo(data):
|
||||
"""Open fifo named pipe in a new thread using a temporary file path. The
|
||||
thread blocks until data is read from the pipe.
|
||||
|
||||
Returns the path to the fifo.
|
||||
|
||||
:param data(bytes): Data to write to the pipe.
|
||||
"""
|
||||
path = os.path.join(tempfile.mkdtemp(), next(tempfile._get_candidate_names()))
|
||||
os.mkfifo(path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
|
||||
threading.Thread(
|
||||
target=lambda p, d: open(p, 'wb').write(d),
|
||||
args=(path, data)
|
||||
).start()
|
||||
return path
|
||||
|
||||
|
||||
def aim_backend(**kwargs):
|
||||
url = kwargs['url']
|
||||
client_cert = kwargs.get('client_cert', None)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
from .plugin import CredentialPlugin
|
||||
|
||||
import base64
|
||||
import os
|
||||
import stat
|
||||
import tempfile
|
||||
import threading
|
||||
from urllib.parse import urljoin, quote_plus
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import requests
|
||||
|
||||
# AWX
|
||||
from awx.main.utils import (
|
||||
create_temporary_fifo,
|
||||
)
|
||||
|
||||
|
||||
conjur_inputs = {
|
||||
'fields': [{
|
||||
@@ -51,24 +52,6 @@ conjur_inputs = {
|
||||
}
|
||||
|
||||
|
||||
def create_temporary_fifo(data):
|
||||
"""Open fifo named pipe in a new thread using a temporary file path. The
|
||||
thread blocks until data is read from the pipe.
|
||||
|
||||
Returns the path to the fifo.
|
||||
|
||||
:param data(bytes): Data to write to the pipe.
|
||||
"""
|
||||
path = os.path.join(tempfile.mkdtemp(), next(tempfile._get_candidate_names()))
|
||||
os.mkfifo(path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
|
||||
threading.Thread(
|
||||
target=lambda p, d: open(p, 'wb').write(d),
|
||||
args=(path, data)
|
||||
).start()
|
||||
return path
|
||||
|
||||
|
||||
def conjur_backend(**kwargs):
|
||||
url = kwargs['url']
|
||||
api_key = kwargs['api_key']
|
||||
|
||||
@@ -8,6 +8,10 @@ from .plugin import CredentialPlugin
|
||||
import requests
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# AWX
|
||||
from awx.main.utils import (
|
||||
create_temporary_fifo,
|
||||
)
|
||||
|
||||
base_inputs = {
|
||||
'fields': [{
|
||||
@@ -22,12 +26,18 @@ base_inputs = {
|
||||
'type': 'string',
|
||||
'secret': True,
|
||||
'help_text': _('The access token used to authenticate to the Vault server'),
|
||||
}, {
|
||||
'id': 'cacert',
|
||||
'label': _('CA Certificate'),
|
||||
'type': 'string',
|
||||
'multiline': True,
|
||||
'help_text': _('The CA certificate used to verify the SSL certificate of the Vault server')
|
||||
}],
|
||||
'metadata': [{
|
||||
'id': 'secret_path',
|
||||
'label': _('Path to Secret'),
|
||||
'type': 'string',
|
||||
'help_text': _('The path to the secret e.g., /some-engine/some-secret/'),
|
||||
'help_text': _('The path to the secret stored in the secret backend e.g, /some/secret/')
|
||||
}],
|
||||
'required': ['url', 'token', 'secret_path'],
|
||||
}
|
||||
@@ -40,7 +50,12 @@ hashi_kv_inputs['fields'].append({
|
||||
'help_text': _('API v1 is for static key/value lookups. API v2 is for versioned key/value lookups.'),
|
||||
'default': 'v1',
|
||||
})
|
||||
hashi_kv_inputs['metadata'].extend([{
|
||||
hashi_kv_inputs['metadata'] = [{
|
||||
'id': 'secret_backend',
|
||||
'label': _('Name of Secret Backend'),
|
||||
'type': 'string',
|
||||
'help_text': _('The name of the kv secret backend (if left empty, the first segment of the secret path will be used).')
|
||||
}] + hashi_kv_inputs['metadata'] + [{
|
||||
'id': 'secret_key',
|
||||
'label': _('Key Name'),
|
||||
'type': 'string',
|
||||
@@ -50,7 +65,7 @@ hashi_kv_inputs['metadata'].extend([{
|
||||
'label': _('Secret Version (v2 only)'),
|
||||
'type': 'string',
|
||||
'help_text': _('Used to specify a specific secret version (if left empty, the latest version will be used).'),
|
||||
}])
|
||||
}]
|
||||
hashi_kv_inputs['required'].extend(['api_version', 'secret_key'])
|
||||
|
||||
hashi_ssh_inputs = copy.deepcopy(base_inputs)
|
||||
@@ -75,36 +90,46 @@ hashi_ssh_inputs['required'].extend(['public_key', 'role'])
|
||||
|
||||
def kv_backend(**kwargs):
|
||||
token = kwargs['token']
|
||||
url = urljoin(kwargs['url'], 'v1')
|
||||
url = kwargs['url']
|
||||
secret_path = kwargs['secret_path']
|
||||
secret_backend = kwargs.get('secret_backend', None)
|
||||
secret_key = kwargs.get('secret_key', None)
|
||||
|
||||
cacert = kwargs.get('cacert', None)
|
||||
api_version = kwargs['api_version']
|
||||
|
||||
request_kwargs = {'timeout': 30}
|
||||
if cacert:
|
||||
request_kwargs['verify'] = create_temporary_fifo(cacert.encode())
|
||||
|
||||
sess = requests.Session()
|
||||
sess.headers['Authorization'] = 'Bearer {}'.format(token)
|
||||
|
||||
if api_version == 'v2':
|
||||
params = {}
|
||||
if kwargs.get('secret_version'):
|
||||
params['version'] = kwargs['secret_version']
|
||||
try:
|
||||
mount_point, *path = pathlib.Path(secret_path.lstrip(os.sep)).parts
|
||||
'/'.join(*path)
|
||||
except Exception:
|
||||
mount_point, path = secret_path, []
|
||||
# https://www.vaultproject.io/api/secret/kv/kv-v2.html#read-secret-version
|
||||
response = sess.get(
|
||||
'/'.join([url, mount_point, 'data'] + path).rstrip('/'),
|
||||
params=params,
|
||||
timeout=30
|
||||
)
|
||||
response.raise_for_status()
|
||||
json = response.json()['data']
|
||||
request_kwargs['params'] = {'version': kwargs['secret_version']}
|
||||
if secret_backend:
|
||||
path_segments = [secret_backend, 'data', secret_path]
|
||||
else:
|
||||
try:
|
||||
mount_point, *path = pathlib.Path(secret_path.lstrip(os.sep)).parts
|
||||
'/'.join(path)
|
||||
except Exception:
|
||||
mount_point, path = secret_path, []
|
||||
# https://www.vaultproject.io/api/secret/kv/kv-v2.html#read-secret-version
|
||||
path_segments = [mount_point, 'data'] + path
|
||||
else:
|
||||
# https://www.vaultproject.io/api/secret/kv/kv-v1.html#read-secret
|
||||
response = sess.get('/'.join([url, secret_path]).rstrip('/'), timeout=30)
|
||||
response.raise_for_status()
|
||||
json = response.json()
|
||||
if secret_backend:
|
||||
path_segments = [secret_backend, secret_path]
|
||||
else:
|
||||
path_segments = [secret_path]
|
||||
|
||||
request_url = urljoin(url, '/'.join(['v1'] + path_segments)).rstrip('/')
|
||||
response = sess.get(request_url, **request_kwargs)
|
||||
response.raise_for_status()
|
||||
|
||||
json = response.json()
|
||||
if api_version == 'v2':
|
||||
json = json['data']
|
||||
|
||||
if secret_key:
|
||||
try:
|
||||
@@ -121,20 +146,22 @@ def ssh_backend(**kwargs):
|
||||
url = urljoin(kwargs['url'], 'v1')
|
||||
secret_path = kwargs['secret_path']
|
||||
role = kwargs['role']
|
||||
cacert = kwargs.get('cacert', None)
|
||||
|
||||
request_kwargs = {'timeout': 30}
|
||||
if cacert:
|
||||
request_kwargs['verify'] = create_temporary_fifo(cacert.encode())
|
||||
|
||||
request_kwargs['json'] = {'public_key': kwargs['public_key']}
|
||||
if kwargs.get('valid_principals'):
|
||||
request_kwargs['json']['valid_principals'] = kwargs['valid_principals']
|
||||
|
||||
sess = requests.Session()
|
||||
sess.headers['Authorization'] = 'Bearer {}'.format(token)
|
||||
json = {
|
||||
'public_key': kwargs['public_key']
|
||||
}
|
||||
if kwargs.get('valid_principals'):
|
||||
json['valid_principals'] = kwargs['valid_principals']
|
||||
# https://www.vaultproject.io/api/secret/ssh/index.html#sign-ssh-key
|
||||
resp = sess.post(
|
||||
'/'.join([url, secret_path, 'sign', role]).rstrip('/'),
|
||||
json=json,
|
||||
timeout=30
|
||||
)
|
||||
request_url = '/'.join([url, secret_path, 'sign', role]).rstrip('/')
|
||||
resp = sess.post(request_url, **request_kwargs)
|
||||
|
||||
resp.raise_for_status()
|
||||
return resp.json()['data']['signed_key']
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ from jinja2 import Environment, StrictUndefined
|
||||
from jinja2.exceptions import UndefinedError, TemplateSyntaxError
|
||||
|
||||
# Django
|
||||
import django
|
||||
from django.contrib.postgres.fields import JSONField as upstream_JSONBField
|
||||
from django.core import exceptions as django_exceptions
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models.signals import (
|
||||
post_save,
|
||||
post_delete,
|
||||
@@ -37,7 +38,6 @@ import jsonschema.exceptions
|
||||
|
||||
# Django-JSONField
|
||||
from jsonfield import JSONField as upstream_JSONField
|
||||
from jsonbfield.fields import JSONField as upstream_JSONBField
|
||||
|
||||
# DRF
|
||||
from rest_framework import serializers
|
||||
@@ -76,10 +76,10 @@ class JSONField(upstream_JSONField):
|
||||
def db_type(self, connection):
|
||||
return 'text'
|
||||
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value in {'', None} and not self.null:
|
||||
return {}
|
||||
return super(JSONField, self).from_db_value(value, expression, connection, context)
|
||||
return super(JSONField, self).from_db_value(value, expression, connection)
|
||||
|
||||
|
||||
class JSONBField(upstream_JSONBField):
|
||||
@@ -91,12 +91,12 @@ class JSONBField(upstream_JSONBField):
|
||||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
if connection.vendor == 'sqlite':
|
||||
# sqlite (which we use for tests) does not support jsonb;
|
||||
return json.dumps(value)
|
||||
return json.dumps(value, cls=DjangoJSONEncoder)
|
||||
return super(JSONBField, self).get_db_prep_value(
|
||||
value, connection, prepared
|
||||
)
|
||||
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
def from_db_value(self, value, expression, connection):
|
||||
# Work around a bug in django-jsonfield
|
||||
# https://bitbucket.org/schinckel/django-jsonfield/issues/57/cannot-use-in-the-same-project-as-djangos
|
||||
if isinstance(value, str):
|
||||
@@ -112,14 +112,9 @@ class AutoSingleRelatedObjectDescriptor(ReverseOneToOneDescriptor):
|
||||
|
||||
def __get__(self, instance, instance_type=None):
|
||||
try:
|
||||
return super(AutoSingleRelatedObjectDescriptor,
|
||||
self).__get__(instance, instance_type)
|
||||
return super(AutoSingleRelatedObjectDescriptor, self).__get__(instance, instance_type)
|
||||
except self.related.related_model.DoesNotExist:
|
||||
obj = self.related.related_model(**{self.related.field.name: instance})
|
||||
if self.related.field.rel.parent_link:
|
||||
raise NotImplementedError('not supported with polymorphic!')
|
||||
for f in instance._meta.local_fields:
|
||||
setattr(obj, f.name, getattr(instance, f.name))
|
||||
obj.save()
|
||||
return obj
|
||||
|
||||
@@ -453,21 +448,6 @@ class JSONSchemaField(JSONBField):
|
||||
params={'value': value},
|
||||
)
|
||||
|
||||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
if connection.vendor == 'sqlite':
|
||||
# sqlite (which we use for tests) does not support jsonb;
|
||||
return json.dumps(value)
|
||||
return super(JSONSchemaField, self).get_db_prep_value(
|
||||
value, connection, prepared
|
||||
)
|
||||
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
# Work around a bug in django-jsonfield
|
||||
# https://bitbucket.org/schinckel/django-jsonfield/issues/57/cannot-use-in-the-same-project-as-djangos
|
||||
if isinstance(value, str):
|
||||
return json.loads(value)
|
||||
return value
|
||||
|
||||
|
||||
@JSONSchemaField.format_checker.checks('vault_id')
|
||||
def format_vault_id(value):
|
||||
@@ -986,7 +966,7 @@ class OAuth2ClientSecretField(models.CharField):
|
||||
encrypt_value(value), connection, prepared
|
||||
)
|
||||
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value and value.startswith('$encrypted$'):
|
||||
return decrypt_value(get_encryption_key('value', pk=None), value)
|
||||
return value
|
||||
@@ -1022,38 +1002,6 @@ class OrderedManyToManyDescriptor(ManyToManyDescriptor):
|
||||
'%s__position' % self.through._meta.model_name
|
||||
)
|
||||
|
||||
def add(self, *objs):
|
||||
# Django < 2 doesn't support this method on
|
||||
# ManyToManyFields w/ an intermediary model
|
||||
# We should be able to remove this code snippet when we
|
||||
# upgrade Django.
|
||||
# see: https://github.com/django/django/blob/stable/1.11.x/django/db/models/fields/related_descriptors.py#L926
|
||||
if not django.__version__.startswith('1.'):
|
||||
raise RuntimeError(
|
||||
'This method is no longer necessary in Django>=2'
|
||||
)
|
||||
try:
|
||||
self.through._meta.auto_created = True
|
||||
super(OrderedManyRelatedManager, self).add(*objs)
|
||||
finally:
|
||||
self.through._meta.auto_created = False
|
||||
|
||||
def remove(self, *objs):
|
||||
# Django < 2 doesn't support this method on
|
||||
# ManyToManyFields w/ an intermediary model
|
||||
# We should be able to remove this code snippet when we
|
||||
# upgrade Django.
|
||||
# see: https://github.com/django/django/blob/stable/1.11.x/django/db/models/fields/related_descriptors.py#L944
|
||||
if not django.__version__.startswith('1.'):
|
||||
raise RuntimeError(
|
||||
'This method is no longer necessary in Django>=2'
|
||||
)
|
||||
try:
|
||||
self.through._meta.auto_created = True
|
||||
super(OrderedManyRelatedManager, self).remove(*objs)
|
||||
finally:
|
||||
self.through._meta.auto_created = False
|
||||
|
||||
return OrderedManyRelatedManager
|
||||
|
||||
return add_custom_queryset_to_many_related_manager(
|
||||
|
||||
@@ -73,7 +73,7 @@ class ActivityStreamMiddleware(threading.local, MiddlewareMixin):
|
||||
super().__init__(get_response)
|
||||
|
||||
def process_request(self, request):
|
||||
if hasattr(request, 'user') and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated():
|
||||
if hasattr(request, 'user') and request.user.is_authenticated:
|
||||
user = request.user
|
||||
else:
|
||||
user = None
|
||||
|
||||
@@ -44,7 +44,7 @@ class Migration(migrations.Migration):
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('host_name', models.CharField(default='', max_length=1024, editable=False)),
|
||||
('event', models.CharField(max_length=100, choices=[('runner_on_failed', 'Host Failed'), ('runner_on_ok', 'Host OK'), ('runner_on_unreachable', 'Host Unreachable'), ('runner_on_skipped', 'Host Skipped')])),
|
||||
('event_data', jsonfield.fields.JSONField(default={}, blank=True)),
|
||||
('event_data', jsonfield.fields.JSONField(default=dict, blank=True)),
|
||||
('failed', models.BooleanField(default=False, editable=False)),
|
||||
('changed', models.BooleanField(default=False, editable=False)),
|
||||
('counter', models.PositiveIntegerField(default=0)),
|
||||
@@ -62,7 +62,7 @@ class Migration(migrations.Migration):
|
||||
('expires', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('request_hash', models.CharField(default='', max_length=40, blank=True)),
|
||||
('reason', models.CharField(default='', help_text='Reason the auth token was invalidated.', max_length=1024, blank=True)),
|
||||
('user', models.ForeignKey(related_name='auth_tokens', to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.ForeignKey(related_name='auth_tokens', on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
@@ -198,7 +198,7 @@ class Migration(migrations.Migration):
|
||||
('created', models.DateTimeField(default=None, editable=False)),
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('event', models.CharField(max_length=100, choices=[('runner_on_failed', 'Host Failed'), ('runner_on_ok', 'Host OK'), ('runner_on_error', 'Host Failure'), ('runner_on_skipped', 'Host Skipped'), ('runner_on_unreachable', 'Host Unreachable'), ('runner_on_no_hosts', 'No Hosts Remaining'), ('runner_on_async_poll', 'Host Polling'), ('runner_on_async_ok', 'Host Async OK'), ('runner_on_async_failed', 'Host Async Failure'), ('runner_on_file_diff', 'File Difference'), ('playbook_on_start', 'Playbook Started'), ('playbook_on_notify', 'Running Handlers'), ('playbook_on_no_hosts_matched', 'No Hosts Matched'), ('playbook_on_no_hosts_remaining', 'No Hosts Remaining'), ('playbook_on_task_start', 'Task Started'), ('playbook_on_vars_prompt', 'Variables Prompted'), ('playbook_on_setup', 'Gathering Facts'), ('playbook_on_import_for_host', 'internal: on Import for Host'), ('playbook_on_not_import_for_host', 'internal: on Not Import for Host'), ('playbook_on_play_start', 'Play Started'), ('playbook_on_stats', 'Playbook Complete')])),
|
||||
('event_data', jsonfield.fields.JSONField(default={}, blank=True)),
|
||||
('event_data', jsonfield.fields.JSONField(default=dict, blank=True)),
|
||||
('failed', models.BooleanField(default=False, editable=False)),
|
||||
('changed', models.BooleanField(default=False, editable=False)),
|
||||
('host_name', models.CharField(default='', max_length=1024, editable=False)),
|
||||
@@ -241,7 +241,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('modified', models.DateTimeField(auto_now=True)),
|
||||
('instance', models.ForeignKey(to='main.Instance')),
|
||||
('instance', models.ForeignKey(on_delete=models.CASCADE, to='main.Instance')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
@@ -287,7 +287,7 @@ class Migration(migrations.Migration):
|
||||
('created', models.DateTimeField(default=None, editable=False)),
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('ldap_dn', models.CharField(default='', max_length=1024)),
|
||||
('user', awx.main.fields.AutoOneToOneField(related_name='profile', editable=False, to=settings.AUTH_USER_MODEL)),
|
||||
('user', awx.main.fields.AutoOneToOneField(related_name='profile', editable=False, on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
@@ -304,7 +304,7 @@ class Migration(migrations.Migration):
|
||||
('dtend', models.DateTimeField(default=None, null=True, editable=False)),
|
||||
('rrule', models.CharField(max_length=255)),
|
||||
('next_run', models.DateTimeField(default=None, null=True, editable=False)),
|
||||
('extra_data', jsonfield.fields.JSONField(default={}, blank=True)),
|
||||
('extra_data', jsonfield.fields.JSONField(default=dict, blank=True)),
|
||||
('created_by', models.ForeignKey(related_name="{u'class': 'schedule', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('modified_by', models.ForeignKey(related_name="{u'class': 'schedule', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')),
|
||||
@@ -343,7 +343,7 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(max_length=512)),
|
||||
('old_pk', models.PositiveIntegerField(default=None, null=True, editable=False)),
|
||||
('launch_type', models.CharField(default='manual', max_length=20, editable=False, choices=[('manual', 'Manual'), ('relaunch', 'Relaunch'), ('callback', 'Callback'), ('scheduled', 'Scheduled'), ('dependency', 'Dependency')])),
|
||||
('cancel_flag', models.BooleanField(default=False, editable=False)),
|
||||
('cancel_flag', models.BooleanField(blank=True, default=False, editable=False)),
|
||||
('status', models.CharField(default='new', max_length=20, editable=False, choices=[('new', 'New'), ('pending', 'Pending'), ('waiting', 'Waiting'), ('running', 'Running'), ('successful', 'Successful'), ('failed', 'Failed'), ('error', 'Error'), ('canceled', 'Canceled')])),
|
||||
('failed', models.BooleanField(default=False, editable=False)),
|
||||
('started', models.DateTimeField(default=None, null=True, editable=False)),
|
||||
@@ -351,7 +351,7 @@ class Migration(migrations.Migration):
|
||||
('elapsed', models.DecimalField(editable=False, max_digits=12, decimal_places=3)),
|
||||
('job_args', models.TextField(default='', editable=False, blank=True)),
|
||||
('job_cwd', models.CharField(default='', max_length=1024, editable=False, blank=True)),
|
||||
('job_env', jsonfield.fields.JSONField(default={}, editable=False, blank=True)),
|
||||
('job_env', jsonfield.fields.JSONField(default=dict, editable=False, blank=True)),
|
||||
('job_explanation', models.TextField(default='', editable=False, blank=True)),
|
||||
('start_args', models.TextField(default='', editable=False, blank=True)),
|
||||
('result_stdout_text', models.TextField(default='', editable=False, blank=True)),
|
||||
@@ -380,7 +380,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='AdHocCommand',
|
||||
fields=[
|
||||
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')),
|
||||
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, on_delete=django.db.models.deletion.CASCADE, serialize=False, to='main.UnifiedJob')),
|
||||
('job_type', models.CharField(default='run', max_length=64, choices=[('run', 'Run'), ('check', 'Check')])),
|
||||
('limit', models.CharField(default='', max_length=1024, blank=True)),
|
||||
('module_name', models.CharField(default='', max_length=1024, blank=True)),
|
||||
@@ -394,7 +394,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='InventorySource',
|
||||
fields=[
|
||||
('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJobTemplate')),
|
||||
('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, on_delete=django.db.models.deletion.CASCADE, serialize=False, to='main.UnifiedJobTemplate')),
|
||||
('source', models.CharField(default='', max_length=32, blank=True, choices=[('', 'Manual'), ('file', 'Local File, Directory or Script'), ('rax', 'Rackspace Cloud Servers'), ('ec2', 'Amazon EC2'), ('gce', 'Google Compute Engine'), ('azure', 'Microsoft Azure'), ('vmware', 'VMware vCenter'), ('openstack', 'OpenStack'), ('custom', 'Custom Script')])),
|
||||
('source_path', models.CharField(default='', max_length=1024, editable=False, blank=True)),
|
||||
('source_vars', models.TextField(default='', help_text='Inventory source variables in YAML or JSON format.', blank=True)),
|
||||
@@ -411,7 +411,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='InventoryUpdate',
|
||||
fields=[
|
||||
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')),
|
||||
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, on_delete=django.db.models.deletion.CASCADE, serialize=False, to='main.UnifiedJob')),
|
||||
('source', models.CharField(default='', max_length=32, blank=True, choices=[('', 'Manual'), ('file', 'Local File, Directory or Script'), ('rax', 'Rackspace Cloud Servers'), ('ec2', 'Amazon EC2'), ('gce', 'Google Compute Engine'), ('azure', 'Microsoft Azure'), ('vmware', 'VMware vCenter'), ('openstack', 'OpenStack'), ('custom', 'Custom Script')])),
|
||||
('source_path', models.CharField(default='', max_length=1024, editable=False, blank=True)),
|
||||
('source_vars', models.TextField(default='', help_text='Inventory source variables in YAML or JSON format.', blank=True)),
|
||||
@@ -427,7 +427,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='Job',
|
||||
fields=[
|
||||
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')),
|
||||
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, on_delete=django.db.models.deletion.CASCADE, serialize=False, to='main.UnifiedJob')),
|
||||
('job_type', models.CharField(default='run', max_length=64, choices=[('run', 'Run'), ('check', 'Check'), ('scan', 'Scan')])),
|
||||
('playbook', models.CharField(default='', max_length=1024, blank=True)),
|
||||
('forks', models.PositiveIntegerField(default=0, blank=True)),
|
||||
@@ -435,7 +435,7 @@ class Migration(migrations.Migration):
|
||||
('verbosity', models.PositiveIntegerField(default=0, blank=True, choices=[(0, '0 (Normal)'), (1, '1 (Verbose)'), (2, '2 (More Verbose)'), (3, '3 (Debug)'), (4, '4 (Connection Debug)'), (5, '5 (WinRM Debug)')])),
|
||||
('extra_vars', models.TextField(default='', blank=True)),
|
||||
('job_tags', models.CharField(default='', max_length=1024, blank=True)),
|
||||
('force_handlers', models.BooleanField(default=False)),
|
||||
('force_handlers', models.BooleanField(blank=True, default=False)),
|
||||
('skip_tags', models.CharField(default='', max_length=1024, blank=True)),
|
||||
('start_at_task', models.CharField(default='', max_length=1024, blank=True)),
|
||||
('become_enabled', models.BooleanField(default=False)),
|
||||
@@ -448,7 +448,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='JobTemplate',
|
||||
fields=[
|
||||
('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJobTemplate')),
|
||||
('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, on_delete=django.db.models.deletion.CASCADE, serialize=False, to='main.UnifiedJobTemplate')),
|
||||
('job_type', models.CharField(default='run', max_length=64, choices=[('run', 'Run'), ('check', 'Check'), ('scan', 'Scan')])),
|
||||
('playbook', models.CharField(default='', max_length=1024, blank=True)),
|
||||
('forks', models.PositiveIntegerField(default=0, blank=True)),
|
||||
@@ -456,14 +456,14 @@ class Migration(migrations.Migration):
|
||||
('verbosity', models.PositiveIntegerField(default=0, blank=True, choices=[(0, '0 (Normal)'), (1, '1 (Verbose)'), (2, '2 (More Verbose)'), (3, '3 (Debug)'), (4, '4 (Connection Debug)'), (5, '5 (WinRM Debug)')])),
|
||||
('extra_vars', models.TextField(default='', blank=True)),
|
||||
('job_tags', models.CharField(default='', max_length=1024, blank=True)),
|
||||
('force_handlers', models.BooleanField(default=False)),
|
||||
('force_handlers', models.BooleanField(blank=True, default=False)),
|
||||
('skip_tags', models.CharField(default='', max_length=1024, blank=True)),
|
||||
('start_at_task', models.CharField(default='', max_length=1024, blank=True)),
|
||||
('become_enabled', models.BooleanField(default=False)),
|
||||
('host_config_key', models.CharField(default='', max_length=1024, blank=True)),
|
||||
('ask_variables_on_launch', models.BooleanField(default=False)),
|
||||
('survey_enabled', models.BooleanField(default=False)),
|
||||
('survey_spec', jsonfield.fields.JSONField(default={}, blank=True)),
|
||||
('survey_spec', jsonfield.fields.JSONField(default=dict, blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('name',),
|
||||
@@ -473,7 +473,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='Project',
|
||||
fields=[
|
||||
('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJobTemplate')),
|
||||
('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, on_delete=django.db.models.deletion.CASCADE, serialize=False, to='main.UnifiedJobTemplate')),
|
||||
('local_path', models.CharField(help_text='Local path (relative to PROJECTS_ROOT) containing playbooks and related files for this project.', max_length=1024, blank=True)),
|
||||
('scm_type', models.CharField(default='', max_length=8, verbose_name='SCM Type', blank=True, choices=[('', 'Manual'), ('git', 'Git'), ('hg', 'Mercurial'), ('svn', 'Subversion')])),
|
||||
('scm_url', models.CharField(default='', max_length=1024, verbose_name='SCM URL', blank=True)),
|
||||
@@ -492,7 +492,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='ProjectUpdate',
|
||||
fields=[
|
||||
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')),
|
||||
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, on_delete=django.db.models.deletion.CASCADE, serialize=False, to='main.UnifiedJob')),
|
||||
('local_path', models.CharField(help_text='Local path (relative to PROJECTS_ROOT) containing playbooks and related files for this project.', max_length=1024, blank=True)),
|
||||
('scm_type', models.CharField(default='', max_length=8, verbose_name='SCM Type', blank=True, choices=[('', 'Manual'), ('git', 'Git'), ('hg', 'Mercurial'), ('svn', 'Subversion')])),
|
||||
('scm_url', models.CharField(default='', max_length=1024, verbose_name='SCM URL', blank=True)),
|
||||
@@ -505,7 +505,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='SystemJob',
|
||||
fields=[
|
||||
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')),
|
||||
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, on_delete=django.db.models.deletion.CASCADE, serialize=False, to='main.UnifiedJob')),
|
||||
('job_type', models.CharField(default='', max_length=32, blank=True, choices=[('cleanup_jobs', 'Remove jobs older than a certain number of days'), ('cleanup_activitystream', 'Remove activity stream entries older than a certain number of days'), ('cleanup_deleted', 'Purge previously deleted items from the database'), ('cleanup_facts', 'Purge and/or reduce the granularity of system tracking data')])),
|
||||
('extra_vars', models.TextField(default='', blank=True)),
|
||||
],
|
||||
@@ -517,7 +517,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='SystemJobTemplate',
|
||||
fields=[
|
||||
('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJobTemplate')),
|
||||
('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, on_delete=django.db.models.deletion.CASCADE, serialize=False, to='main.UnifiedJobTemplate')),
|
||||
('job_type', models.CharField(default='', max_length=32, blank=True, choices=[('cleanup_jobs', 'Remove jobs older than a certain number of days'), ('cleanup_activitystream', 'Remove activity stream entries older than a certain number of days'), ('cleanup_deleted', 'Purge previously deleted items from the database'), ('cleanup_facts', 'Purge and/or reduce the granularity of system tracking data')])),
|
||||
],
|
||||
bases=('main.unifiedjobtemplate', models.Model),
|
||||
@@ -550,7 +550,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='unifiedjobtemplate',
|
||||
name='polymorphic_ctype',
|
||||
field=models.ForeignKey(related_name='polymorphic_main.unifiedjobtemplate_set+', editable=False, to='contenttypes.ContentType', null=True),
|
||||
field=models.ForeignKey(related_name='polymorphic_main.unifiedjobtemplate_set+', editable=False, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unifiedjobtemplate',
|
||||
@@ -575,7 +575,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='unifiedjob',
|
||||
name='polymorphic_ctype',
|
||||
field=models.ForeignKey(related_name='polymorphic_main.unifiedjob_set+', editable=False, to='contenttypes.ContentType', null=True),
|
||||
field=models.ForeignKey(related_name='polymorphic_main.unifiedjob_set+', editable=False, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unifiedjob',
|
||||
@@ -595,7 +595,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='schedule',
|
||||
name='unified_job_template',
|
||||
field=models.ForeignKey(related_name='schedules', to='main.UnifiedJobTemplate'),
|
||||
field=models.ForeignKey(related_name='schedules', on_delete=django.db.models.deletion.CASCADE, to='main.UnifiedJobTemplate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='permission',
|
||||
@@ -610,12 +610,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='joborigin',
|
||||
name='unified_job',
|
||||
field=models.OneToOneField(related_name='job_origin', to='main.UnifiedJob'),
|
||||
field=models.OneToOneField(related_name='job_origin', on_delete=django.db.models.deletion.CASCADE, to='main.UnifiedJob'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='organization',
|
||||
field=models.ForeignKey(related_name='inventories', to='main.Organization', help_text='Organization containing this inventory.'),
|
||||
field=models.ForeignKey(related_name='inventories', on_delete=django.db.models.deletion.CASCADE, to='main.Organization', help_text='Organization containing this inventory.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
@@ -625,7 +625,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='host',
|
||||
name='inventory',
|
||||
field=models.ForeignKey(related_name='hosts', to='main.Inventory'),
|
||||
field=models.ForeignKey(related_name='hosts', on_delete=django.db.models.deletion.CASCADE, to='main.Inventory'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='host',
|
||||
@@ -650,7 +650,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='inventory',
|
||||
field=models.ForeignKey(related_name='groups', to='main.Inventory'),
|
||||
field=models.ForeignKey(related_name='groups', on_delete=django.db.models.deletion.CASCADE, to='main.Inventory'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
@@ -680,12 +680,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='team',
|
||||
field=models.ForeignKey(related_name='credentials', default=None, blank=True, to='main.Team', null=True),
|
||||
field=models.ForeignKey(related_name='credentials', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Team', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='user',
|
||||
field=models.ForeignKey(related_name='credentials', default=None, blank=True, to=settings.AUTH_USER_MODEL, null=True),
|
||||
field=models.ForeignKey(related_name='credentials', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to=settings.AUTH_USER_MODEL, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adhoccommandevent',
|
||||
@@ -774,7 +774,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='projectupdate',
|
||||
name='project',
|
||||
field=models.ForeignKey(related_name='project_updates', editable=False, to='main.Project'),
|
||||
field=models.ForeignKey(related_name='project_updates', on_delete=django.db.models.deletion.CASCADE, editable=False, to='main.Project'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
@@ -814,12 +814,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='jobhostsummary',
|
||||
name='job',
|
||||
field=models.ForeignKey(related_name='job_host_summaries', editable=False, to='main.Job'),
|
||||
field=models.ForeignKey(related_name='job_host_summaries', on_delete=django.db.models.deletion.CASCADE, editable=False, to='main.Job'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobevent',
|
||||
name='job',
|
||||
field=models.ForeignKey(related_name='job_events', editable=False, to='main.Job'),
|
||||
field=models.ForeignKey(related_name='job_events', on_delete=django.db.models.deletion.CASCADE, editable=False, to='main.Job'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='job',
|
||||
@@ -859,7 +859,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='inventoryupdate',
|
||||
name='inventory_source',
|
||||
field=models.ForeignKey(related_name='inventory_updates', editable=False, to='main.InventorySource'),
|
||||
field=models.ForeignKey(related_name='inventory_updates', on_delete=django.db.models.deletion.CASCADE, editable=False, to='main.InventorySource'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryupdate',
|
||||
@@ -874,12 +874,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='inventorysource',
|
||||
name='group',
|
||||
field=awx.main.fields.AutoOneToOneField(related_name='inventory_source', null=True, default=None, editable=False, to='main.Group'),
|
||||
field=awx.main.fields.AutoOneToOneField(related_name='inventory_source', on_delete=django.db.models.deletion.SET_NULL, null=True, default=None, editable=False, to='main.Group'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventorysource',
|
||||
name='inventory',
|
||||
field=models.ForeignKey(related_name='inventory_sources', default=None, editable=False, to='main.Inventory', null=True),
|
||||
field=models.ForeignKey(related_name='inventory_sources', on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to='main.Inventory', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventorysource',
|
||||
@@ -916,7 +916,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='adhoccommandevent',
|
||||
name='ad_hoc_command',
|
||||
field=models.ForeignKey(related_name='ad_hoc_command_events', editable=False, to='main.AdHocCommand'),
|
||||
field=models.ForeignKey(related_name='ad_hoc_command_events', on_delete=django.db.models.deletion.CASCADE, editable=False, to='main.AdHocCommand'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adhoccommand',
|
||||
|
||||
@@ -13,7 +13,6 @@ from django.conf import settings
|
||||
from django.utils.timezone import now
|
||||
|
||||
import jsonfield.fields
|
||||
import jsonbfield.fields
|
||||
import taggit.managers
|
||||
|
||||
|
||||
@@ -144,7 +143,7 @@ class Migration(migrations.Migration):
|
||||
('category', models.CharField(max_length=128)),
|
||||
('value', models.TextField(blank=True)),
|
||||
('value_type', models.CharField(max_length=12, choices=[('string', 'String'), ('int', 'Integer'), ('float', 'Decimal'), ('json', 'JSON'), ('bool', 'Boolean'), ('password', 'Password'), ('list', 'List')])),
|
||||
('user', models.ForeignKey(related_name='settings', default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('user', models.ForeignKey(related_name='settings', default=None, editable=False, to=settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)),
|
||||
],
|
||||
),
|
||||
# Notification changes
|
||||
@@ -185,7 +184,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='notification',
|
||||
name='notification_template',
|
||||
field=models.ForeignKey(related_name='notifications', editable=False, to='main.NotificationTemplate'),
|
||||
field=models.ForeignKey(related_name='notifications', editable=False, on_delete=models.CASCADE, to='main.NotificationTemplate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='activitystream',
|
||||
@@ -239,8 +238,8 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('timestamp', models.DateTimeField(default=None, help_text='Date and time of the corresponding fact scan gathering time.', editable=False)),
|
||||
('module', models.CharField(max_length=128)),
|
||||
('facts', jsonbfield.fields.JSONField(default={}, help_text='Arbitrary JSON structure of module facts captured at timestamp for a single host.', blank=True)),
|
||||
('host', models.ForeignKey(related_name='facts', to='main.Host', help_text='Host for the facts that the fact scan captured.')),
|
||||
('facts', awx.main.fields.JSONBField(default=dict, help_text='Arbitrary JSON structure of module facts captured at timestamp for a single host.', blank=True)),
|
||||
('host', models.ForeignKey(related_name='facts', to='main.Host', on_delete=models.CASCADE, help_text='Host for the facts that the fact scan captured.')),
|
||||
],
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
@@ -318,7 +317,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='organization',
|
||||
field=models.ForeignKey(related_name='projects', to='main.Organization', blank=True, null=True),
|
||||
field=models.ForeignKey(related_name='projects', to='main.Organization', on_delete=models.CASCADE, blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
@@ -367,7 +366,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='organization',
|
||||
field=models.ForeignKey(related_name='credentials', default=None, blank=True, to='main.Organization', null=True),
|
||||
field=models.ForeignKey(related_name='credentials', on_delete=models.CASCADE, default=None, blank=True, to='main.Organization', null=True),
|
||||
),
|
||||
|
||||
#
|
||||
@@ -382,7 +381,7 @@ class Migration(migrations.Migration):
|
||||
('members', models.ManyToManyField(related_name='roles', to=settings.AUTH_USER_MODEL)),
|
||||
('parents', models.ManyToManyField(related_name='children', to='main.Role')),
|
||||
('implicit_parents', models.TextField(default='[]')),
|
||||
('content_type', models.ForeignKey(default=None, to='contenttypes.ContentType', null=True)),
|
||||
('content_type', models.ForeignKey(default=None, to='contenttypes.ContentType', on_delete=models.CASCADE, null=True)),
|
||||
('object_id', models.PositiveIntegerField(default=None, null=True)),
|
||||
|
||||
],
|
||||
@@ -398,8 +397,8 @@ class Migration(migrations.Migration):
|
||||
('role_field', models.TextField()),
|
||||
('content_type_id', models.PositiveIntegerField()),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('ancestor', models.ForeignKey(related_name='+', to='main.Role')),
|
||||
('descendent', models.ForeignKey(related_name='+', to='main.Role')),
|
||||
('ancestor', models.ForeignKey(on_delete=models.CASCADE, related_name='+', to='main.Role')),
|
||||
('descendent', models.ForeignKey(on_delete=models.CASCADE, related_name='+', to='main.Role')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'main_rbac_role_ancestors',
|
||||
@@ -569,7 +568,7 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(max_length=512)),
|
||||
('created_by', models.ForeignKey(related_name="{u'class': 'label', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('modified_by', models.ForeignKey(related_name="{u'class': 'label', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('organization', models.ForeignKey(related_name='labels', to='main.Organization', help_text='Organization this label belongs to.')),
|
||||
('organization', models.ForeignKey(related_name='labels', on_delete=django.db.models.deletion.CASCADE, to='main.Organization', help_text='Organization this label belongs to.')),
|
||||
('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')),
|
||||
],
|
||||
options={
|
||||
@@ -599,12 +598,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='label',
|
||||
name='organization',
|
||||
field=models.ForeignKey(related_name='labels', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Organization', help_text='Organization this label belongs to.', null=True),
|
||||
field=models.ForeignKey(related_name='labels', on_delete=django.db.models.deletion.CASCADE, default=None, blank=True, to='main.Organization', help_text='Organization this label belongs to.', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='label',
|
||||
name='organization',
|
||||
field=models.ForeignKey(related_name='labels', to='main.Organization', help_text='Organization this label belongs to.'),
|
||||
field=models.ForeignKey(related_name='labels', on_delete=django.db.models.deletion.CASCADE, to='main.Organization', help_text='Organization this label belongs to.'),
|
||||
),
|
||||
# InventorySource Credential
|
||||
migrations.AddField(
|
||||
@@ -630,12 +629,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='credential',
|
||||
name='deprecated_team',
|
||||
field=models.ForeignKey(related_name='deprecated_credentials', default=None, blank=True, to='main.Team', null=True),
|
||||
field=models.ForeignKey(related_name='deprecated_credentials', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Team', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='credential',
|
||||
name='deprecated_user',
|
||||
field=models.ForeignKey(related_name='deprecated_credentials', default=None, blank=True, to=settings.AUTH_USER_MODEL, null=True),
|
||||
field=models.ForeignKey(related_name='deprecated_credentials', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to=settings.AUTH_USER_MODEL, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='credential',
|
||||
|
||||
@@ -116,7 +116,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='organization',
|
||||
field=models.ForeignKey(related_name='teams', to='main.Organization'),
|
||||
field=models.ForeignKey(related_name='teams', on_delete=models.CASCADE, to='main.Organization'),
|
||||
preserve_default=False,
|
||||
),
|
||||
] + _squashed.operations(SQUASHED_30, applied=True)
|
||||
|
||||
@@ -74,7 +74,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='WorkflowJob',
|
||||
fields=[
|
||||
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')),
|
||||
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, on_delete=models.CASCADE, primary_key=True, serialize=False, to='main.UnifiedJob')),
|
||||
('extra_vars', models.TextField(default='', blank=True)),
|
||||
],
|
||||
options={
|
||||
@@ -100,7 +100,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='WorkflowJobTemplate',
|
||||
fields=[
|
||||
('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJobTemplate')),
|
||||
('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, on_delete=models.CASCADE, serialize=False, to='main.UnifiedJobTemplate')),
|
||||
('extra_vars', models.TextField(default='', blank=True)),
|
||||
('admin_role', awx.main.fields.ImplicitRoleField(related_name='+', parent_role='singleton:system_administrator', to='main.Role', null='True')),
|
||||
],
|
||||
@@ -116,7 +116,7 @@ class Migration(migrations.Migration):
|
||||
('failure_nodes', models.ManyToManyField(related_name='workflowjobtemplatenodes_failure', to='main.WorkflowJobTemplateNode', blank=True)),
|
||||
('success_nodes', models.ManyToManyField(related_name='workflowjobtemplatenodes_success', to='main.WorkflowJobTemplateNode', blank=True)),
|
||||
('unified_job_template', models.ForeignKey(related_name='workflowjobtemplatenodes', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.UnifiedJobTemplate', null=True)),
|
||||
('workflow_job_template', models.ForeignKey(related_name='workflow_job_template_nodes', default=None, blank=True, to='main.WorkflowJobTemplate', null=True)),
|
||||
('workflow_job_template', models.ForeignKey(related_name='workflow_job_template_nodes', on_delete=models.SET_NULL, default=None, blank=True, to='main.WorkflowJobTemplate', null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
@@ -161,7 +161,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='workflowjobnode',
|
||||
name='char_prompts',
|
||||
field=jsonfield.fields.JSONField(default={}, blank=True),
|
||||
field=jsonfield.fields.JSONField(default=dict, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowjobnode',
|
||||
@@ -191,7 +191,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='workflowjobtemplatenode',
|
||||
name='char_prompts',
|
||||
field=jsonfield.fields.JSONField(default={}, blank=True),
|
||||
field=jsonfield.fields.JSONField(default=dict, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowjobtemplatenode',
|
||||
@@ -211,7 +211,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='workflowjobnode',
|
||||
name='workflow_job',
|
||||
field=models.ForeignKey(related_name='workflow_job_nodes', default=None, blank=True, to='main.WorkflowJob', null=True),
|
||||
field=models.ForeignKey(related_name='workflow_job_nodes', on_delete=django.db.models.deletion.CASCADE, default=None, blank=True, to='main.WorkflowJob', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='workflowjobtemplate',
|
||||
@@ -227,12 +227,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='job',
|
||||
name='artifacts',
|
||||
field=jsonfield.fields.JSONField(default={}, editable=False, blank=True),
|
||||
field=jsonfield.fields.JSONField(default=dict, editable=False, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowjobnode',
|
||||
name='ancestor_artifacts',
|
||||
field=jsonfield.fields.JSONField(default={}, editable=False, blank=True),
|
||||
field=jsonfield.fields.JSONField(default=dict, editable=False, blank=True),
|
||||
),
|
||||
# Job timeout settings
|
||||
migrations.AddField(
|
||||
@@ -397,7 +397,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='workflowjob',
|
||||
name='survey_passwords',
|
||||
field=jsonfield.fields.JSONField(default={}, editable=False, blank=True),
|
||||
field=jsonfield.fields.JSONField(default=dict, editable=False, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowjobtemplate',
|
||||
@@ -407,33 +407,33 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='workflowjobtemplate',
|
||||
name='survey_spec',
|
||||
field=jsonfield.fields.JSONField(default={}, blank=True),
|
||||
field=jsonfield.fields.JSONField(default=dict, blank=True),
|
||||
),
|
||||
# JSON field changes
|
||||
migrations.AlterField(
|
||||
model_name='adhoccommandevent',
|
||||
name='event_data',
|
||||
field=awx.main.fields.JSONField(default={}, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='job',
|
||||
name='artifacts',
|
||||
field=awx.main.fields.JSONField(default={}, editable=False, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, editable=False, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='job',
|
||||
name='survey_passwords',
|
||||
field=awx.main.fields.JSONField(default={}, editable=False, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, editable=False, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobevent',
|
||||
name='event_data',
|
||||
field=awx.main.fields.JSONField(default={}, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobtemplate',
|
||||
name='survey_spec',
|
||||
field=awx.main.fields.JSONField(default={}, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
@@ -453,37 +453,37 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='schedule',
|
||||
name='extra_data',
|
||||
field=awx.main.fields.JSONField(default={}, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unifiedjob',
|
||||
name='job_env',
|
||||
field=awx.main.fields.JSONField(default={}, editable=False, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, editable=False, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='workflowjob',
|
||||
name='survey_passwords',
|
||||
field=awx.main.fields.JSONField(default={}, editable=False, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, editable=False, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='workflowjobnode',
|
||||
name='ancestor_artifacts',
|
||||
field=awx.main.fields.JSONField(default={}, editable=False, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, editable=False, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='workflowjobnode',
|
||||
name='char_prompts',
|
||||
field=awx.main.fields.JSONField(default={}, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='workflowjobtemplate',
|
||||
name='survey_spec',
|
||||
field=awx.main.fields.JSONField(default={}, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='workflowjobtemplatenode',
|
||||
name='char_prompts',
|
||||
field=awx.main.fields.JSONField(default={}, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, blank=True),
|
||||
),
|
||||
# Job Project Update
|
||||
migrations.AddField(
|
||||
|
||||
@@ -55,12 +55,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='inventorysource',
|
||||
name='deprecated_group',
|
||||
field=models.OneToOneField(related_name='deprecated_inventory_source', null=True, default=None, to='main.Group'),
|
||||
field=models.OneToOneField(related_name='deprecated_inventory_source', on_delete=models.CASCADE, null=True, default=None, to='main.Group'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventorysource',
|
||||
name='inventory',
|
||||
field=models.ForeignKey(related_name='inventory_sources', default=None, to='main.Inventory', null=True),
|
||||
field=models.ForeignKey(related_name='inventory_sources', default=None, to='main.Inventory', on_delete=models.CASCADE, null=True),
|
||||
),
|
||||
|
||||
# Smart Inventory
|
||||
@@ -78,13 +78,13 @@ class Migration(migrations.Migration):
|
||||
name='SmartInventoryMembership',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('host', models.ForeignKey(related_name='+', to='main.Host')),
|
||||
('host', models.ForeignKey(related_name='+', on_delete=models.CASCADE, to='main.Host')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='smartinventorymembership',
|
||||
name='inventory',
|
||||
field=models.ForeignKey(related_name='+', to='main.Inventory'),
|
||||
field=models.ForeignKey(on_delete=models.CASCADE, related_name='+', to='main.Inventory'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='host',
|
||||
@@ -105,19 +105,19 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='inventory',
|
||||
name='organization',
|
||||
field=models.ForeignKey(related_name='inventories', on_delete=models.deletion.SET_NULL, to='main.Organization', help_text='Organization containing this inventory.', null=True),
|
||||
field=models.ForeignKey(related_name='inventories', on_delete=models.SET_NULL, to='main.Organization', help_text='Organization containing this inventory.', null=True),
|
||||
),
|
||||
|
||||
# Facts
|
||||
migrations.AlterField(
|
||||
model_name='fact',
|
||||
name='facts',
|
||||
field=awx.main.fields.JSONBField(default={}, help_text='Arbitrary JSON structure of module facts captured at timestamp for a single host.', blank=True),
|
||||
field=awx.main.fields.JSONBField(default=dict, help_text='Arbitrary JSON structure of module facts captured at timestamp for a single host.', blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='host',
|
||||
name='ansible_facts',
|
||||
field=awx.main.fields.JSONBField(default={}, help_text='Arbitrary JSON structure of most recent ansible_facts, per-host.', blank=True),
|
||||
field=awx.main.fields.JSONBField(default=dict, help_text='Arbitrary JSON structure of most recent ansible_facts, per-host.', blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='host',
|
||||
@@ -148,12 +148,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='inventorysource',
|
||||
name='source_project',
|
||||
field=models.ForeignKey(related_name='scm_inventory_sources', default=None, blank=True, to='main.Project', help_text='Project containing inventory file used as source.', null=True),
|
||||
field=models.ForeignKey(related_name='scm_inventory_sources', on_delete=models.CASCADE, default=None, blank=True, to='main.Project', help_text='Project containing inventory file used as source.', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryupdate',
|
||||
name='source_project_update',
|
||||
field=models.ForeignKey(related_name='scm_inventory_updates', default=None, blank=True, to='main.ProjectUpdate', help_text='Inventory files from this Project Update were used for the inventory update.', null=True),
|
||||
field=models.ForeignKey(related_name='scm_inventory_updates', on_delete=models.CASCADE, default=None, blank=True, to='main.ProjectUpdate', help_text='Inventory files from this Project Update were used for the inventory update.', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
@@ -200,7 +200,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='notificationtemplate',
|
||||
name='organization',
|
||||
field=models.ForeignKey(related_name='notification_templates', to='main.Organization', null=True),
|
||||
field=models.ForeignKey(related_name='notification_templates', on_delete=models.CASCADE, to='main.Organization', null=True),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='notificationtemplate',
|
||||
@@ -312,7 +312,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='insights_credential',
|
||||
field=models.ForeignKey(related_name='insights_inventories', on_delete=models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', help_text='Credentials to be used by hosts belonging to this inventory when accessing Red Hat Insights API.', null=True),
|
||||
field=models.ForeignKey(related_name='insights_inventories', on_delete=models.SET_NULL, default=None, blank=True, to='main.Credential', help_text='Credentials to be used by hosts belonging to this inventory when accessing Red Hat Insights API.', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventory',
|
||||
@@ -382,10 +382,10 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(max_length=512)),
|
||||
('kind', models.CharField(max_length=32, choices=[('ssh', 'Machine'), ('vault', 'Vault'), ('net', 'Network'), ('scm', 'Source Control'), ('cloud', 'Cloud'), ('insights', 'Insights')])),
|
||||
('managed_by_tower', models.BooleanField(default=False, editable=False)),
|
||||
('inputs', awx.main.fields.CredentialTypeInputField(default={}, blank=True, help_text='Enter inputs using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.')),
|
||||
('injectors', awx.main.fields.CredentialTypeInjectorField(default={}, blank=True, help_text='Enter injectors using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.')),
|
||||
('created_by', models.ForeignKey(related_name="{u'class': 'credentialtype', u'app_label': 'main'}(class)s_created+", on_delete=models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('modified_by', models.ForeignKey(related_name="{u'class': 'credentialtype', u'app_label': 'main'}(class)s_modified+", on_delete=models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('inputs', awx.main.fields.CredentialTypeInputField(default=dict, blank=True, help_text='Enter inputs using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.')),
|
||||
('injectors', awx.main.fields.CredentialTypeInjectorField(default=dict, blank=True, help_text='Enter injectors using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.')),
|
||||
('created_by', models.ForeignKey(related_name="{u'class': 'credentialtype', u'app_label': 'main'}(class)s_created+", on_delete=models.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('modified_by', models.ForeignKey(related_name="{u'class': 'credentialtype', u'app_label': 'main'}(class)s_modified+", on_delete=models.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')),
|
||||
],
|
||||
options={
|
||||
@@ -399,23 +399,23 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='inputs',
|
||||
field=awx.main.fields.CredentialInputField(default={}, blank=True),
|
||||
field=awx.main.fields.CredentialInputField(default=dict, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='credential',
|
||||
name='credential_type',
|
||||
field=models.ForeignKey(related_name='credentials', to='main.CredentialType', null=True),
|
||||
field=models.ForeignKey(related_name='credentials', on_delete=models.CASCADE, to='main.CredentialType', null=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='job',
|
||||
name='vault_credential',
|
||||
field=models.ForeignKey(related_name='jobs_as_vault_credential+', on_delete=models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', null=True),
|
||||
field=models.ForeignKey(related_name='jobs_as_vault_credential+', on_delete=models.SET_NULL, default=None, blank=True, to='main.Credential', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobtemplate',
|
||||
name='vault_credential',
|
||||
field=models.ForeignKey(related_name='jobtemplates_as_vault_credential+', on_delete=models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', null=True),
|
||||
field=models.ForeignKey(related_name='jobtemplates_as_vault_credential+', on_delete=models.SET_NULL, default=None, blank=True, to='main.Credential', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='job',
|
||||
@@ -452,7 +452,7 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(unique=True, max_length=250)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('modified', models.DateTimeField(auto_now=True)),
|
||||
('controller', models.ForeignKey(related_name='controlled_groups', default=None, editable=False, to='main.InstanceGroup', help_text='Instance Group to remotely control this group.', null=True)),
|
||||
('controller', models.ForeignKey(related_name='controlled_groups', on_delete=models.CASCADE, default=None, editable=False, to='main.InstanceGroup', help_text='Instance Group to remotely control this group.', null=True)),
|
||||
('instances', models.ManyToManyField(help_text='Instances that are members of this InstanceGroup', related_name='rampart_groups', editable=False, to='main.Instance')),
|
||||
],
|
||||
),
|
||||
@@ -464,7 +464,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='unifiedjob',
|
||||
name='instance_group',
|
||||
field=models.ForeignKey(on_delete=models.deletion.SET_NULL, default=None, blank=True, to='main.InstanceGroup', help_text='The Rampart/Instance group the job was run under', null=True),
|
||||
field=models.ForeignKey(on_delete=models.SET_NULL, default=None, blank=True, to='main.InstanceGroup', help_text='The Rampart/Instance group the job was run under', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unifiedjobtemplate',
|
||||
|
||||
@@ -103,12 +103,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='credential',
|
||||
name='credential_type',
|
||||
field=models.ForeignKey(related_name='credentials', to='main.CredentialType', null=False, help_text='Specify the type of credential you want to create. Refer to the Ansible Tower documentation for details on each type.')
|
||||
field=models.ForeignKey(related_name='credentials', to='main.CredentialType', on_delete=models.CASCADE, null=False, help_text='Specify the type of credential you want to create. Refer to the Ansible Tower documentation for details on each type.')
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='credential',
|
||||
name='inputs',
|
||||
field=awx.main.fields.CredentialInputField(default={}, help_text='Enter inputs using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.', blank=True),
|
||||
field=awx.main.fields.CredentialInputField(default=dict, help_text='Enter inputs using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.', blank=True),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='job',
|
||||
|
||||
@@ -20,7 +20,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='schedule',
|
||||
name='char_prompts',
|
||||
field=awx.main.fields.JSONField(default={}, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='schedule',
|
||||
@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='schedule',
|
||||
name='survey_passwords',
|
||||
field=awx.main.fields.JSONField(default={}, editable=False, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, editable=False, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowjobnode',
|
||||
@@ -45,12 +45,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='workflowjobnode',
|
||||
name='extra_data',
|
||||
field=awx.main.fields.JSONField(default={}, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowjobnode',
|
||||
name='survey_passwords',
|
||||
field=awx.main.fields.JSONField(default={}, editable=False, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, editable=False, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowjobtemplatenode',
|
||||
@@ -60,12 +60,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='workflowjobtemplatenode',
|
||||
name='extra_data',
|
||||
field=awx.main.fields.JSONField(default={}, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowjobtemplatenode',
|
||||
name='survey_passwords',
|
||||
field=awx.main.fields.JSONField(default={}, editable=False, blank=True),
|
||||
field=awx.main.fields.JSONField(default=dict, editable=False, blank=True),
|
||||
),
|
||||
# Run data migration before removing the old credential field
|
||||
migrations.RunPython(migration_utils.set_current_apps_for_migrations, migrations.RunPython.noop),
|
||||
@@ -83,9 +83,9 @@ class Migration(migrations.Migration):
|
||||
name='JobLaunchConfig',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('extra_data', awx.main.fields.JSONField(blank=True, default={})),
|
||||
('survey_passwords', awx.main.fields.JSONField(blank=True, default={}, editable=False)),
|
||||
('char_prompts', awx.main.fields.JSONField(blank=True, default={})),
|
||||
('extra_data', awx.main.fields.JSONField(blank=True, default=dict)),
|
||||
('survey_passwords', awx.main.fields.JSONField(blank=True, default=dict, editable=False)),
|
||||
('char_prompts', awx.main.fields.JSONField(blank=True, default=dict)),
|
||||
('credentials', models.ManyToManyField(related_name='joblaunchconfigs', to='main.Credential')),
|
||||
('inventory', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='joblaunchconfigs', to='main.Inventory')),
|
||||
('job', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='launch_config', to='main.UnifiedJob')),
|
||||
@@ -94,51 +94,51 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='workflowjobtemplate',
|
||||
name='ask_variables_on_launch',
|
||||
field=awx.main.fields.AskForField(default=False),
|
||||
field=awx.main.fields.AskForField(blank=True, default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobtemplate',
|
||||
name='ask_credential_on_launch',
|
||||
field=awx.main.fields.AskForField(default=False),
|
||||
field=awx.main.fields.AskForField(blank=True, default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobtemplate',
|
||||
name='ask_diff_mode_on_launch',
|
||||
field=awx.main.fields.AskForField(default=False),
|
||||
field=awx.main.fields.AskForField(blank=True, default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobtemplate',
|
||||
name='ask_inventory_on_launch',
|
||||
field=awx.main.fields.AskForField(default=False),
|
||||
field=awx.main.fields.AskForField(blank=True, default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobtemplate',
|
||||
name='ask_job_type_on_launch',
|
||||
field=awx.main.fields.AskForField(default=False),
|
||||
field=awx.main.fields.AskForField(blank=True, default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobtemplate',
|
||||
name='ask_limit_on_launch',
|
||||
field=awx.main.fields.AskForField(default=False),
|
||||
field=awx.main.fields.AskForField(blank=True, default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobtemplate',
|
||||
name='ask_skip_tags_on_launch',
|
||||
field=awx.main.fields.AskForField(default=False),
|
||||
field=awx.main.fields.AskForField(blank=True, default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobtemplate',
|
||||
name='ask_tags_on_launch',
|
||||
field=awx.main.fields.AskForField(default=False),
|
||||
field=awx.main.fields.AskForField(blank=True, default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobtemplate',
|
||||
name='ask_variables_on_launch',
|
||||
field=awx.main.fields.AskForField(default=False),
|
||||
field=awx.main.fields.AskForField(blank=True, default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobtemplate',
|
||||
name='ask_verbosity_on_launch',
|
||||
field=awx.main.fields.AskForField(default=False),
|
||||
field=awx.main.fields.AskForField(blank=True, default=False),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -20,7 +20,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(default=None, editable=False)),
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('event_data', awx.main.fields.JSONField(blank=True, default={})),
|
||||
('event_data', awx.main.fields.JSONField(blank=True, default=dict)),
|
||||
('uuid', models.CharField(default='', editable=False, max_length=1024)),
|
||||
('counter', models.PositiveIntegerField(default=0, editable=False)),
|
||||
('stdout', models.TextField(default='', editable=False)),
|
||||
@@ -40,7 +40,7 @@ class Migration(migrations.Migration):
|
||||
('created', models.DateTimeField(default=None, editable=False)),
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('event', models.CharField(choices=[('runner_on_failed', 'Host Failed'), ('runner_on_ok', 'Host OK'), ('runner_on_error', 'Host Failure'), ('runner_on_skipped', 'Host Skipped'), ('runner_on_unreachable', 'Host Unreachable'), ('runner_on_no_hosts', 'No Hosts Remaining'), ('runner_on_async_poll', 'Host Polling'), ('runner_on_async_ok', 'Host Async OK'), ('runner_on_async_failed', 'Host Async Failure'), ('runner_item_on_ok', 'Item OK'), ('runner_item_on_failed', 'Item Failed'), ('runner_item_on_skipped', 'Item Skipped'), ('runner_retry', 'Host Retry'), ('runner_on_file_diff', 'File Difference'), ('playbook_on_start', 'Playbook Started'), ('playbook_on_notify', 'Running Handlers'), ('playbook_on_include', 'Including File'), ('playbook_on_no_hosts_matched', 'No Hosts Matched'), ('playbook_on_no_hosts_remaining', 'No Hosts Remaining'), ('playbook_on_task_start', 'Task Started'), ('playbook_on_vars_prompt', 'Variables Prompted'), ('playbook_on_setup', 'Gathering Facts'), ('playbook_on_import_for_host', 'internal: on Import for Host'), ('playbook_on_not_import_for_host', 'internal: on Not Import for Host'), ('playbook_on_play_start', 'Play Started'), ('playbook_on_stats', 'Playbook Complete'), ('debug', 'Debug'), ('verbose', 'Verbose'), ('deprecated', 'Deprecated'), ('warning', 'Warning'), ('system_warning', 'System Warning'), ('error', 'Error')], max_length=100)),
|
||||
('event_data', awx.main.fields.JSONField(blank=True, default={})),
|
||||
('event_data', awx.main.fields.JSONField(blank=True, default=dict)),
|
||||
('failed', models.BooleanField(default=False, editable=False)),
|
||||
('changed', models.BooleanField(default=False, editable=False)),
|
||||
('uuid', models.CharField(default='', editable=False, max_length=1024)),
|
||||
@@ -65,7 +65,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(default=None, editable=False)),
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('event_data', awx.main.fields.JSONField(blank=True, default={})),
|
||||
('event_data', awx.main.fields.JSONField(blank=True, default=dict)),
|
||||
('uuid', models.CharField(default='', editable=False, max_length=1024)),
|
||||
('counter', models.PositiveIntegerField(default=0, editable=False)),
|
||||
('stdout', models.TextField(default='', editable=False)),
|
||||
|
||||
@@ -17,7 +17,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='workflowjob',
|
||||
name='char_prompts',
|
||||
field=awx.main.fields.JSONField(blank=True, default={}),
|
||||
field=awx.main.fields.JSONField(blank=True, default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowjob',
|
||||
@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='workflowjobtemplate',
|
||||
name='ask_inventory_on_launch',
|
||||
field=awx.main.fields.AskForField(default=False),
|
||||
field=awx.main.fields.AskForField(blank=True, default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workflowjobtemplate',
|
||||
|
||||
@@ -34,7 +34,7 @@ class Migration(migrations.Migration):
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('description', models.TextField(blank=True, default='')),
|
||||
('input_field_name', models.CharField(max_length=1024)),
|
||||
('metadata', awx.main.fields.DynamicCredentialInputField(blank=True, default={})),
|
||||
('metadata', awx.main.fields.DynamicCredentialInputField(blank=True, default=dict)),
|
||||
('created_by', models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{'class': 'credentialinputsource', 'model_name': 'credentialinputsource', 'app_label': 'main'}(class)s_created+", to=settings.AUTH_USER_MODEL)),
|
||||
('modified_by', models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="{'class': 'credentialinputsource', 'model_name': 'credentialinputsource', 'app_label': 'main'}(class)s_modified+", to=settings.AUTH_USER_MODEL)),
|
||||
('source_credential', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='target_input_sources', to='main.Credential')),
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import logging
|
||||
from time import time
|
||||
|
||||
from django.utils.encoding import smart_text
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
from collections import defaultdict
|
||||
from awx.main.utils import getattrd
|
||||
from awx.main.models.rbac import Role, batch_role_ancestor_rebuilding
|
||||
|
||||
logger = logging.getLogger('rbac_migrations')
|
||||
|
||||
|
||||
def create_roles(apps, schema_editor):
|
||||
'''
|
||||
Implicit role creation happens in our post_save hook for all of our
|
||||
@@ -41,465 +35,19 @@ def create_roles(apps, schema_editor):
|
||||
obj.save()
|
||||
|
||||
|
||||
def migrate_users(apps, schema_editor):
|
||||
User = apps.get_model('auth', "User")
|
||||
Role = apps.get_model('main', "Role")
|
||||
ContentType = apps.get_model('contenttypes', "ContentType")
|
||||
user_content_type = ContentType.objects.get_for_model(User)
|
||||
|
||||
for user in User.objects.iterator():
|
||||
user.save()
|
||||
try:
|
||||
Role.objects.get(content_type=user_content_type, object_id=user.id)
|
||||
logger.info(smart_text(u"found existing role for user: {}".format(user.username)))
|
||||
except Role.DoesNotExist:
|
||||
role = Role.objects.create(
|
||||
role_field='admin_role',
|
||||
content_type = user_content_type,
|
||||
object_id = user.id
|
||||
)
|
||||
role.members.add(user)
|
||||
logger.info(smart_text(u"migrating to new role for user: {}".format(user.username)))
|
||||
|
||||
if user.is_superuser:
|
||||
if Role.objects.filter(singleton_name='system_administrator').exists():
|
||||
sa_role = Role.objects.get(singleton_name='system_administrator')
|
||||
else:
|
||||
sa_role = Role.objects.create(
|
||||
singleton_name='system_administrator',
|
||||
role_field='system_administrator'
|
||||
)
|
||||
|
||||
sa_role.members.add(user)
|
||||
logger.warning(smart_text(u"added superuser: {}".format(user.username)))
|
||||
|
||||
def migrate_organization(apps, schema_editor):
|
||||
Organization = apps.get_model('main', "Organization")
|
||||
for org in Organization.objects.iterator():
|
||||
for admin in org.deprecated_admins.all():
|
||||
org.admin_role.members.add(admin)
|
||||
logger.info(smart_text(u"added admin: {}, {}".format(org.name, admin.username)))
|
||||
for user in org.deprecated_users.all():
|
||||
org.member_role.members.add(user)
|
||||
logger.info(smart_text(u"added member: {}, {}".format(org.name, user.username)))
|
||||
|
||||
def migrate_team(apps, schema_editor):
|
||||
Team = apps.get_model('main', 'Team')
|
||||
for t in Team.objects.iterator():
|
||||
for user in t.deprecated_users.all():
|
||||
t.member_role.members.add(user)
|
||||
logger.info(smart_text(u"team: {}, added user: {}".format(t.name, user.username)))
|
||||
|
||||
def attrfunc(attr_path):
|
||||
'''attrfunc returns a function that will
|
||||
attempt to use the attr_path to access the attribute
|
||||
of an instance that is passed in to the returned function.
|
||||
|
||||
Example:
|
||||
get_org = attrfunc('inventory.organization')
|
||||
org = get_org(JobTemplateInstance)
|
||||
'''
|
||||
def attr(inst):
|
||||
return getattrd(inst, attr_path)
|
||||
return attr
|
||||
|
||||
def _update_credential_parents(org, cred):
|
||||
cred.organization = org
|
||||
cred.save()
|
||||
|
||||
def _discover_credentials(instances, cred, orgfunc):
|
||||
'''_discover_credentials will find shared credentials across
|
||||
organizations. If a shared credential is found, it will duplicate
|
||||
the credential, ensure the proper role permissions are added to the new
|
||||
credential, and update any references from the old to the newly created
|
||||
credential.
|
||||
|
||||
instances is a list of all objects that were matched when filtered
|
||||
with cred.
|
||||
|
||||
orgfunc is a function that when called with an instance from instances
|
||||
will produce an Organization object.
|
||||
'''
|
||||
orgs = defaultdict(list)
|
||||
for inst in instances:
|
||||
try:
|
||||
orgs[orgfunc(inst)].append(inst)
|
||||
except AttributeError:
|
||||
# JobTemplate.inventory can be NULL sometimes, eg when an inventory
|
||||
# has been deleted. This protects against that.
|
||||
pass
|
||||
|
||||
if len(orgs) == 1:
|
||||
try:
|
||||
_update_credential_parents(orgfunc(instances[0]), cred)
|
||||
except AttributeError:
|
||||
# JobTemplate.inventory can be NULL sometimes, eg when an inventory
|
||||
# has been deleted. This protects against that.
|
||||
pass
|
||||
else:
|
||||
for pos, org in enumerate(orgs):
|
||||
if pos == 0:
|
||||
_update_credential_parents(org, cred)
|
||||
else:
|
||||
# Create a new credential
|
||||
cred.pk = None
|
||||
cred.organization = None
|
||||
cred.save()
|
||||
|
||||
cred.admin_role, cred.use_role = None, None
|
||||
|
||||
for i in orgs[org]:
|
||||
i.credential = cred
|
||||
i.save()
|
||||
|
||||
_update_credential_parents(org, cred)
|
||||
|
||||
def migrate_credential(apps, schema_editor):
|
||||
Credential = apps.get_model('main', "Credential")
|
||||
JobTemplate = apps.get_model('main', 'JobTemplate')
|
||||
Project = apps.get_model('main', 'Project')
|
||||
InventorySource = apps.get_model('main', 'InventorySource')
|
||||
|
||||
for cred in Credential.objects.iterator():
|
||||
results = [x for x in JobTemplate.objects.filter(Q(credential=cred) | Q(cloud_credential=cred), inventory__isnull=False).all()] + \
|
||||
[x for x in InventorySource.objects.filter(credential=cred).all()]
|
||||
if cred.deprecated_team is not None and results:
|
||||
if len(results) == 1:
|
||||
_update_credential_parents(results[0].inventory.organization, cred)
|
||||
else:
|
||||
_discover_credentials(results, cred, attrfunc('inventory.organization'))
|
||||
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at organization level".format(cred.name, cred.kind, cred.host)))
|
||||
|
||||
projs = Project.objects.filter(credential=cred).all()
|
||||
if cred.deprecated_team is not None and projs:
|
||||
if len(projs) == 1:
|
||||
_update_credential_parents(projs[0].organization, cred)
|
||||
else:
|
||||
_discover_credentials(projs, cred, attrfunc('organization'))
|
||||
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at organization level".format(cred.name, cred.kind, cred.host)))
|
||||
|
||||
if cred.deprecated_team is not None:
|
||||
cred.deprecated_team.admin_role.children.add(cred.admin_role)
|
||||
cred.deprecated_team.member_role.children.add(cred.use_role)
|
||||
cred.save()
|
||||
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host)))
|
||||
elif cred.deprecated_user is not None:
|
||||
cred.admin_role.members.add(cred.deprecated_user)
|
||||
cred.save()
|
||||
logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host, )))
|
||||
else:
|
||||
logger.warning(smart_text(u"orphaned credential found Credential(name={}, kind={}, host={}), superuser only".format(cred.name, cred.kind, cred.host, )))
|
||||
|
||||
|
||||
def migrate_inventory(apps, schema_editor):
|
||||
Inventory = apps.get_model('main', 'Inventory')
|
||||
Permission = apps.get_model('main', 'Permission')
|
||||
|
||||
def role_from_permission(perm):
|
||||
if perm.permission_type == 'admin':
|
||||
return inventory.admin_role
|
||||
elif perm.permission_type == 'read':
|
||||
return inventory.read_role
|
||||
elif perm.permission_type == 'write':
|
||||
return inventory.update_role
|
||||
elif perm.permission_type == 'check' or perm.permission_type == 'run' or perm.permission_type == 'create':
|
||||
# These permission types are handled differntly in RBAC now, nothing to migrate.
|
||||
return False
|
||||
else:
|
||||
return None
|
||||
|
||||
for inventory in Inventory.objects.iterator():
|
||||
for perm in Permission.objects.filter(inventory=inventory):
|
||||
role = None
|
||||
execrole = None
|
||||
|
||||
role = role_from_permission(perm)
|
||||
if role is None:
|
||||
raise Exception(smart_text(u'Unhandled permission type for inventory: {}'.format( perm.permission_type)))
|
||||
|
||||
if perm.run_ad_hoc_commands:
|
||||
execrole = inventory.use_role
|
||||
|
||||
if perm.team:
|
||||
if role:
|
||||
perm.team.member_role.children.add(role)
|
||||
if execrole:
|
||||
perm.team.member_role.children.add(execrole)
|
||||
logger.info(smart_text(u'added Team({}) access to Inventory({})'.format(perm.team.name, inventory.name)))
|
||||
|
||||
if perm.user:
|
||||
if role:
|
||||
role.members.add(perm.user)
|
||||
if execrole:
|
||||
execrole.members.add(perm.user)
|
||||
logger.info(smart_text(u'added User({}) access to Inventory({})'.format(perm.user.username, inventory.name)))
|
||||
|
||||
def migrate_projects(apps, schema_editor):
|
||||
'''
|
||||
I can see projects when:
|
||||
X I am a superuser.
|
||||
X I am an admin in an organization associated with the project.
|
||||
X I am a user in an organization associated with the project.
|
||||
X I am on a team associated with the project.
|
||||
X I have been explicitly granted permission to run/check jobs using the
|
||||
project.
|
||||
X I created the project but it isn't associated with an organization
|
||||
I can change/delete when:
|
||||
X I am a superuser.
|
||||
X I am an admin in an organization associated with the project.
|
||||
X I created the project but it isn't associated with an organization
|
||||
'''
|
||||
Project = apps.get_model('main', 'Project')
|
||||
Permission = apps.get_model('main', 'Permission')
|
||||
JobTemplate = apps.get_model('main', 'JobTemplate')
|
||||
|
||||
# Migrate projects to single organizations, duplicating as necessary
|
||||
for project in Project.objects.iterator():
|
||||
original_project_name = project.name
|
||||
project_orgs = project.deprecated_organizations.distinct().all()
|
||||
|
||||
if len(project_orgs) >= 1:
|
||||
first_org = None
|
||||
for org in project_orgs:
|
||||
if first_org is None:
|
||||
# For the first org, re-use our existing Project object, so don't do the below duplication effort
|
||||
first_org = org
|
||||
if len(project_orgs) > 1:
|
||||
project.name = smart_text(u'{} - {}'.format(first_org.name, original_project_name))
|
||||
project.organization = first_org
|
||||
project.save()
|
||||
else:
|
||||
new_prj = Project.objects.create(
|
||||
created = project.created,
|
||||
modified = project.modified,
|
||||
polymorphic_ctype_id = project.polymorphic_ctype_id,
|
||||
description = project.description,
|
||||
name = smart_text(u'{} - {}'.format(org.name, original_project_name)),
|
||||
old_pk = project.old_pk,
|
||||
created_by_id = project.created_by_id,
|
||||
modified_by_id = project.modified_by_id,
|
||||
scm_type = project.scm_type,
|
||||
scm_url = project.scm_url,
|
||||
scm_branch = project.scm_branch,
|
||||
scm_clean = project.scm_clean,
|
||||
scm_delete_on_update = project.scm_delete_on_update,
|
||||
scm_delete_on_next_update = project.scm_delete_on_next_update,
|
||||
scm_update_on_launch = project.scm_update_on_launch,
|
||||
scm_update_cache_timeout = project.scm_update_cache_timeout,
|
||||
credential = project.credential,
|
||||
organization = org
|
||||
)
|
||||
if project.scm_type == "":
|
||||
new_prj.local_path = project.local_path
|
||||
new_prj.save()
|
||||
for team in project.deprecated_teams.iterator():
|
||||
new_prj.deprecated_teams.add(team)
|
||||
logger.warning(smart_text(u'cloning Project({}) onto {} as Project({})'.format(original_project_name, org, new_prj)))
|
||||
job_templates = JobTemplate.objects.filter(project=project, inventory__organization=org).all()
|
||||
for jt in job_templates:
|
||||
jt.project = new_prj
|
||||
jt.save()
|
||||
for perm in Permission.objects.filter(project=project):
|
||||
Permission.objects.create(
|
||||
created = perm.created,
|
||||
modified = perm.modified,
|
||||
created_by = perm.created_by,
|
||||
modified_by = perm.modified_by,
|
||||
description = perm.description,
|
||||
name = perm.name,
|
||||
user = perm.user,
|
||||
team = perm.team,
|
||||
project = new_prj,
|
||||
inventory = perm.inventory,
|
||||
permission_type = perm.permission_type,
|
||||
run_ad_hoc_commands = perm.run_ad_hoc_commands,
|
||||
)
|
||||
|
||||
# Migrate permissions
|
||||
for project in Project.objects.iterator():
|
||||
if project.organization is None and project.created_by is not None:
|
||||
project.admin_role.members.add(project.created_by)
|
||||
logger.warn(smart_text(u'adding Project({}) admin: {}'.format(project.name, project.created_by.username)))
|
||||
|
||||
for team in project.deprecated_teams.all():
|
||||
team.member_role.children.add(project.read_role)
|
||||
logger.info(smart_text(u'adding Team({}) access for Project({})'.format(team.name, project.name)))
|
||||
|
||||
for perm in Permission.objects.filter(project=project):
|
||||
if perm.permission_type == 'create':
|
||||
role = project.use_role
|
||||
else:
|
||||
role = project.read_role
|
||||
|
||||
if perm.team:
|
||||
perm.team.member_role.children.add(role)
|
||||
logger.info(smart_text(u'adding Team({}) access for Project({})'.format(perm.team.name, project.name)))
|
||||
|
||||
if perm.user:
|
||||
role.members.add(perm.user)
|
||||
logger.info(smart_text(u'adding User({}) access for Project({})'.format(perm.user.username, project.name)))
|
||||
|
||||
if project.organization is not None:
|
||||
for user in project.organization.deprecated_users.all():
|
||||
if not (project.use_role.members.filter(pk=user.id).exists() or project.admin_role.members.filter(pk=user.id).exists()):
|
||||
project.read_role.members.add(user)
|
||||
logger.info(smart_text(u'adding Organization({}) member access to Project({})'.format(project.organization.name, project.name)))
|
||||
|
||||
|
||||
|
||||
def migrate_job_templates(apps, schema_editor):
|
||||
'''
|
||||
NOTE: This must be run after orgs, inventory, projects, credential, and
|
||||
users have been migrated
|
||||
'''
|
||||
|
||||
|
||||
'''
|
||||
I can see job templates when:
|
||||
X I am a superuser.
|
||||
- I can read the inventory, project and credential (which means I am an
|
||||
org admin or member of a team with access to all of the above).
|
||||
- I have permission explicitly granted to check/deploy with the inventory
|
||||
and project.
|
||||
|
||||
|
||||
#This does not mean I would be able to launch a job from the template or
|
||||
#edit the template.
|
||||
- access.py can_read for JobTemplate enforces that you can only
|
||||
see it if you can launch it, so the above imply launch too
|
||||
'''
|
||||
|
||||
|
||||
'''
|
||||
Tower administrators, organization administrators, and project
|
||||
administrators, within a project under their purview, may create and modify
|
||||
new job templates for that project.
|
||||
|
||||
When editing a job template, they may select among the inventory groups and
|
||||
credentials in the organization for which they have usage permissions, or
|
||||
they may leave either blank to be selected at runtime.
|
||||
|
||||
Additionally, they may specify one or more users/teams that have execution
|
||||
permission for that job template, among the users/teams that are a member
|
||||
of that project.
|
||||
|
||||
That execution permission is valid irrespective of any explicit permissions
|
||||
the user has or has not been granted to the inventory group or credential
|
||||
specified in the job template.
|
||||
|
||||
'''
|
||||
|
||||
User = apps.get_model('auth', 'User')
|
||||
JobTemplate = apps.get_model('main', 'JobTemplate')
|
||||
Team = apps.get_model('main', 'Team')
|
||||
Permission = apps.get_model('main', 'Permission')
|
||||
Credential = apps.get_model('main', 'Credential')
|
||||
|
||||
jt_queryset = JobTemplate.objects.select_related('inventory', 'project', 'inventory__organization', 'execute_role')
|
||||
|
||||
for jt in jt_queryset.iterator():
|
||||
if jt.inventory is None:
|
||||
# If inventory is None, then only system admins and org admins can
|
||||
# do anything with the JT in 2.4
|
||||
continue
|
||||
|
||||
jt_permission_qs = Permission.objects.filter(
|
||||
inventory=jt.inventory,
|
||||
project=jt.project,
|
||||
)
|
||||
|
||||
inventory_permission_qs = Permission.objects.filter(
|
||||
inventory=jt.inventory,
|
||||
project__isnull=True,
|
||||
)
|
||||
|
||||
team_create_permissions = set(
|
||||
jt_permission_qs
|
||||
.filter(permission_type__in=['create'])
|
||||
.values_list('team__id', flat=True)
|
||||
)
|
||||
team_run_permissions = set(
|
||||
jt_permission_qs
|
||||
.filter(permission_type__in=['check', 'run'] if jt.job_type == 'check' else ['run'])
|
||||
.values_list('team__id', flat=True)
|
||||
)
|
||||
user_create_permissions = set(
|
||||
jt_permission_qs
|
||||
.filter(permission_type__in=['create'])
|
||||
.values_list('user__id', flat=True)
|
||||
)
|
||||
user_run_permissions = set(
|
||||
jt_permission_qs
|
||||
.filter(permission_type__in=['check', 'run'] if jt.job_type == 'check' else ['run'])
|
||||
.values_list('user__id', flat=True)
|
||||
)
|
||||
|
||||
team_inv_permissions = defaultdict(set)
|
||||
user_inv_permissions = defaultdict(set)
|
||||
|
||||
for user_id, team_id, inventory_id in inventory_permission_qs.values_list('user_id', 'team_id', 'inventory_id'):
|
||||
if user_id:
|
||||
user_inv_permissions[user_id].add(inventory_id)
|
||||
if team_id:
|
||||
team_inv_permissions[team_id].add(inventory_id)
|
||||
|
||||
|
||||
for team in Team.objects.filter(id__in=team_create_permissions).iterator():
|
||||
if jt.inventory.id in team_inv_permissions[team.id] and \
|
||||
((not jt.credential and not jt.cloud_credential) or
|
||||
Credential.objects.filter(deprecated_team=team, jobtemplates=jt).exists()):
|
||||
team.member_role.children.add(jt.admin_role)
|
||||
logger.info(smart_text(u'transfering admin access on JobTemplate({}) to Team({})'.format(jt.name, team.name)))
|
||||
for team in Team.objects.filter(id__in=team_run_permissions).iterator():
|
||||
if jt.inventory.id in team_inv_permissions[team.id] and \
|
||||
((not jt.credential and not jt.cloud_credential) or
|
||||
Credential.objects.filter(deprecated_team=team, jobtemplates=jt).exists()):
|
||||
team.member_role.children.add(jt.execute_role)
|
||||
logger.info(smart_text(u'transfering execute access on JobTemplate({}) to Team({})'.format(jt.name, team.name)))
|
||||
|
||||
for user in User.objects.filter(id__in=user_create_permissions).iterator():
|
||||
cred = jt.credential or jt.cloud_credential
|
||||
if (jt.inventory.id in user_inv_permissions[user.id] or
|
||||
any([jt.inventory.id in team_inv_permissions[team.id] for team in user.deprecated_teams.all()])) and \
|
||||
(not cred or cred.deprecated_user == user or
|
||||
(cred.deprecated_team and cred.deprecated_team.deprecated_users.filter(pk=user.id).exists())):
|
||||
jt.admin_role.members.add(user)
|
||||
logger.info(smart_text(u'transfering admin access on JobTemplate({}) to User({})'.format(jt.name, user.username)))
|
||||
for user in User.objects.filter(id__in=user_run_permissions).iterator():
|
||||
cred = jt.credential or jt.cloud_credential
|
||||
|
||||
if (jt.inventory.id in user_inv_permissions[user.id] or
|
||||
any([jt.inventory.id in team_inv_permissions[team.id] for team in user.deprecated_teams.all()])) and \
|
||||
(not cred or cred.deprecated_user == user or
|
||||
(cred.deprecated_team and cred.deprecated_team.deprecated_users.filter(pk=user.id).exists())):
|
||||
jt.execute_role.members.add(user)
|
||||
logger.info(smart_text(u'transfering execute access on JobTemplate({}) to User({})'.format(jt.name, user.username)))
|
||||
|
||||
|
||||
|
||||
def rebuild_role_hierarchy(apps, schema_editor):
|
||||
logger.info('Computing role roots..')
|
||||
start = time()
|
||||
roots = Role.objects \
|
||||
.all() \
|
||||
.values_list('id', flat=True)
|
||||
stop = time()
|
||||
logger.info('Found %d roots in %f seconds, rebuilding ancestry map' % (len(roots), stop - start))
|
||||
start = time()
|
||||
Role.rebuild_role_ancestor_list(roots, [])
|
||||
stop = time()
|
||||
logger.info('Rebuild completed in %f seconds' % (stop - start))
|
||||
logger.info('Done.')
|
||||
|
||||
|
||||
def infer_credential_org_from_team(apps, schema_editor):
|
||||
Credential = apps.get_model('main', "Credential")
|
||||
for cred in Credential.objects.exclude(deprecated_team__isnull=True):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
_update_credential_parents(cred.deprecated_team.organization, cred)
|
||||
except IntegrityError:
|
||||
logger.info("Organization<{}> credential for old Team<{}> credential already created".format(cred.deprecated_team.organization.pk, cred.pk))
|
||||
logger.info('Computing role roots..')
|
||||
start = time()
|
||||
roots = Role.objects \
|
||||
.all() \
|
||||
.values_list('id', flat=True)
|
||||
stop = time()
|
||||
logger.info('Found %d roots in %f seconds, rebuilding ancestry map' % (len(roots), stop - start))
|
||||
start = time()
|
||||
Role.rebuild_role_ancestor_list(roots, [])
|
||||
stop = time()
|
||||
logger.info('Rebuild completed in %f seconds' % (stop - start))
|
||||
logger.info('Done.')
|
||||
|
||||
|
||||
def delete_all_user_roles(apps, schema_editor):
|
||||
|
||||
@@ -30,7 +30,7 @@ SQUASHED_30 = {
|
||||
migrations.AddField(
|
||||
model_name='job',
|
||||
name='survey_passwords',
|
||||
field=jsonfield.fields.JSONField(default={}, editable=False, blank=True),
|
||||
field=jsonfield.fields.JSONField(default=dict, editable=False, blank=True),
|
||||
),
|
||||
],
|
||||
'0031_v302_migrate_survey_passwords': [
|
||||
|
||||
@@ -62,24 +62,6 @@ from awx.main.models.oauth import ( # noqa
|
||||
from oauth2_provider.models import Grant, RefreshToken # noqa -- needed django-oauth-toolkit model migrations
|
||||
|
||||
|
||||
|
||||
# Monkeypatch Django serializer to ignore django-taggit fields (which break
|
||||
# the dumpdata command; see https://github.com/alex/django-taggit/issues/155).
|
||||
from django.core.serializers.python import Serializer as _PythonSerializer
|
||||
_original_handle_m2m_field = _PythonSerializer.handle_m2m_field
|
||||
|
||||
|
||||
def _new_handle_m2m_field(self, obj, field):
|
||||
try:
|
||||
field.rel.through._meta
|
||||
except AttributeError:
|
||||
return
|
||||
return _original_handle_m2m_field(self, obj, field)
|
||||
|
||||
|
||||
_PythonSerializer.handle_m2m_field = _new_handle_m2m_field
|
||||
|
||||
|
||||
# Add custom methods to User model for permissions checks.
|
||||
from django.contrib.auth.models import User # noqa
|
||||
from awx.main.access import ( # noqa
|
||||
@@ -158,7 +140,7 @@ User.add_to_class('is_system_auditor', user_is_system_auditor)
|
||||
|
||||
|
||||
def user_is_in_enterprise_category(user, category):
|
||||
ret = (category,) in user.enterprise_auth.all().values_list('provider') and not user.has_usable_password()
|
||||
ret = (category,) in user.enterprise_auth.values_list('provider') and not user.has_usable_password()
|
||||
# NOTE: this if-else block ensures existing enterprise users are still able to
|
||||
# log in. Remove it in a future release
|
||||
if category == 'radius':
|
||||
|
||||
@@ -105,7 +105,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
||||
)
|
||||
inputs = CredentialInputField(
|
||||
blank=True,
|
||||
default={},
|
||||
default=dict,
|
||||
help_text=_('Enter inputs using either JSON or YAML syntax. Use the '
|
||||
'radio button to toggle between the two. Refer to the '
|
||||
'Ansible Tower documentation for example syntax.')
|
||||
@@ -343,14 +343,14 @@ class CredentialType(CommonModelNameNotUnique):
|
||||
)
|
||||
inputs = CredentialTypeInputField(
|
||||
blank=True,
|
||||
default={},
|
||||
default=dict,
|
||||
help_text=_('Enter inputs using either JSON or YAML syntax. Use the '
|
||||
'radio button to toggle between the two. Refer to the '
|
||||
'Ansible Tower documentation for example syntax.')
|
||||
)
|
||||
injectors = CredentialTypeInjectorField(
|
||||
blank=True,
|
||||
default={},
|
||||
default=dict,
|
||||
help_text=_('Enter injectors using either JSON or YAML syntax. Use the '
|
||||
'radio button to toggle between the two. Refer to the '
|
||||
'Ansible Tower documentation for example syntax.')
|
||||
@@ -1117,7 +1117,7 @@ class CredentialInputSource(PrimordialModel):
|
||||
)
|
||||
metadata = DynamicCredentialInputField(
|
||||
blank=True,
|
||||
default={}
|
||||
default=dict
|
||||
)
|
||||
|
||||
def clean_target_credential(self):
|
||||
|
||||
@@ -39,6 +39,14 @@ def gce(cred, env, private_data_dir):
|
||||
f.close()
|
||||
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
env['GCE_CREDENTIALS_FILE_PATH'] = path
|
||||
env['GCP_SERVICE_ACCOUNT_FILE'] = path
|
||||
|
||||
# Handle env variables for new module types.
|
||||
# This includes gcp_compute inventory plugin and
|
||||
# all new gcp_* modules.
|
||||
env['GCP_AUTH_KIND'] = 'serviceaccount'
|
||||
env['GCP_PROJECT'] = project
|
||||
env['GCP_ENV_TYPE'] = 'tower'
|
||||
return path
|
||||
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ class BasePlaybookEvent(CreatedModifiedModel):
|
||||
)
|
||||
event_data = JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
default=dict,
|
||||
)
|
||||
failed = models.BooleanField(
|
||||
default=False,
|
||||
@@ -567,7 +567,7 @@ class BaseCommandEvent(CreatedModifiedModel):
|
||||
|
||||
event_data = JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
default=dict,
|
||||
)
|
||||
uuid = models.CharField(
|
||||
max_length=1024,
|
||||
|
||||
@@ -650,7 +650,7 @@ class Host(CommonModelNameNotUnique, RelatedJobsMixin):
|
||||
)
|
||||
ansible_facts = JSONBField(
|
||||
blank=True,
|
||||
default={},
|
||||
default=dict,
|
||||
help_text=_('Arbitrary JSON structure of most recent ansible_facts, per-host.'),
|
||||
)
|
||||
ansible_facts_modified = models.DateTimeField(
|
||||
@@ -1991,6 +1991,8 @@ class azure_rm(PluginFileInjector):
|
||||
|
||||
source_vars = inventory_update.source_vars_dict
|
||||
|
||||
ret['fail_on_template_errors'] = False
|
||||
|
||||
group_by_hostvar = {
|
||||
'location': {'prefix': '', 'separator': '', 'key': 'location'},
|
||||
'tag': {'prefix': '', 'separator': '', 'key': 'tags.keys() | list if tags else []'},
|
||||
|
||||
@@ -485,7 +485,7 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
|
||||
)
|
||||
artifacts = JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
default=dict,
|
||||
editable=False,
|
||||
)
|
||||
scm_revision = models.CharField(
|
||||
@@ -847,7 +847,7 @@ class LaunchTimeConfigBase(BaseModel):
|
||||
# This is a solution to the nullable CharField problem, specific to prompting
|
||||
char_prompts = JSONField(
|
||||
blank=True,
|
||||
default={}
|
||||
default=dict
|
||||
)
|
||||
|
||||
def prompts_dict(self, display=False):
|
||||
@@ -927,11 +927,11 @@ class LaunchTimeConfig(LaunchTimeConfigBase):
|
||||
# Special case prompting fields, even more special than the other ones
|
||||
extra_data = JSONField(
|
||||
blank=True,
|
||||
default={}
|
||||
default=dict
|
||||
)
|
||||
survey_passwords = prevent_search(JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
default=dict,
|
||||
editable=False,
|
||||
))
|
||||
# Credentials needed for non-unified job / unified JT models
|
||||
|
||||
@@ -100,7 +100,7 @@ class SurveyJobTemplateMixin(models.Model):
|
||||
)
|
||||
survey_spec = prevent_search(JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
default=dict,
|
||||
))
|
||||
ask_variables_on_launch = AskForField(
|
||||
blank=True,
|
||||
@@ -360,7 +360,7 @@ class SurveyJobMixin(models.Model):
|
||||
|
||||
survey_passwords = prevent_search(JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
default=dict,
|
||||
editable=False,
|
||||
))
|
||||
|
||||
|
||||
@@ -641,7 +641,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
)
|
||||
job_env = prevent_search(JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
default=dict,
|
||||
editable=False,
|
||||
))
|
||||
job_explanation = models.TextField(
|
||||
@@ -1397,6 +1397,13 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
r['{}_user_email'.format(name)] = created_by.email
|
||||
r['{}_user_first_name'.format(name)] = created_by.first_name
|
||||
r['{}_user_last_name'.format(name)] = created_by.last_name
|
||||
|
||||
inventory = getattr_dne(self, 'inventory')
|
||||
if inventory:
|
||||
for name in ('awx', 'tower'):
|
||||
r['{}_inventory_id'.format(name)] = inventory.pk
|
||||
r['{}_inventory_name'.format(name)] = inventory.name
|
||||
|
||||
return r
|
||||
|
||||
def get_queue_name(self):
|
||||
|
||||
@@ -180,7 +180,7 @@ class WorkflowJobNode(WorkflowNodeBase):
|
||||
)
|
||||
ancestor_artifacts = JSONField(
|
||||
blank=True,
|
||||
default={},
|
||||
default=dict,
|
||||
editable=False,
|
||||
)
|
||||
do_not_run = models.BooleanField(
|
||||
|
||||
@@ -828,8 +828,10 @@ class BaseTask(object):
|
||||
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
private_data_files['credentials'][credential] = path
|
||||
for credential, data in private_data.get('certificates', {}).items():
|
||||
name = 'credential_%d-cert.pub' % credential.pk
|
||||
path = os.path.join(private_data_dir, name)
|
||||
artifact_dir = os.path.join(private_data_dir, 'artifacts', str(self.instance.id))
|
||||
if not os.path.exists(artifact_dir):
|
||||
os.makedirs(artifact_dir, mode=0o700)
|
||||
path = os.path.join(artifact_dir, 'ssh_key_data-cert.pub')
|
||||
with open(path, 'w') as f:
|
||||
f.write(data)
|
||||
f.close()
|
||||
@@ -857,10 +859,16 @@ class BaseTask(object):
|
||||
'''
|
||||
process_isolation_params = dict()
|
||||
if self.should_use_proot(instance):
|
||||
show_paths = self.proot_show_paths + [private_data_dir, cwd] + \
|
||||
settings.AWX_PROOT_SHOW_PATHS
|
||||
|
||||
# Help the user out by including the collections path inside the bubblewrap environment
|
||||
if getattr(settings, 'AWX_ANSIBLE_COLLECTIONS_PATHS', []):
|
||||
show_paths.extend(settings.AWX_ANSIBLE_COLLECTIONS_PATHS)
|
||||
process_isolation_params = {
|
||||
'process_isolation': True,
|
||||
'process_isolation_path': settings.AWX_PROOT_BASE_PATH,
|
||||
'process_isolation_show_paths': self.proot_show_paths + [private_data_dir, cwd] + settings.AWX_PROOT_SHOW_PATHS,
|
||||
'process_isolation_show_paths': show_paths,
|
||||
'process_isolation_hide_paths': [
|
||||
settings.AWX_PROOT_BASE_PATH,
|
||||
'/etc/tower',
|
||||
@@ -934,6 +942,11 @@ class BaseTask(object):
|
||||
if self.should_use_proot(instance):
|
||||
env['PROOT_TMP_DIR'] = settings.AWX_PROOT_BASE_PATH
|
||||
env['AWX_PRIVATE_DATA_DIR'] = private_data_dir
|
||||
|
||||
if 'ANSIBLE_COLLECTIONS_PATHS' in env:
|
||||
env['ANSIBLE_COLLECTIONS_PATHS'] += os.pathsep + os.pathsep.join(settings.AWX_ANSIBLE_COLLECTIONS_PATHS)
|
||||
else:
|
||||
env['ANSIBLE_COLLECTIONS_PATHS'] = os.pathsep.join(settings.AWX_ANSIBLE_COLLECTIONS_PATHS)
|
||||
return env
|
||||
|
||||
def should_use_proot(self, instance):
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"AZURE_SUBSCRIPTION_ID": "fooo",
|
||||
"AZURE_CLIENT_ID": "fooo",
|
||||
"AZURE_TENANT": "fooo",
|
||||
"AZURE_SECRET": "fooo",
|
||||
"AZURE_CLOUD_ENVIRONMENT": "fooo",
|
||||
"ANSIBLE_JINJA2_NATIVE": "True",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never"
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||
"AZURE_CLIENT_ID": "fooo",
|
||||
"AZURE_CLOUD_ENVIRONMENT": "fooo",
|
||||
"AZURE_SECRET": "fooo",
|
||||
"AZURE_SUBSCRIPTION_ID": "fooo",
|
||||
"AZURE_TENANT": "fooo"
|
||||
}
|
||||
@@ -4,6 +4,7 @@ default_host_filters: []
|
||||
exclude_host_filters:
|
||||
- resource_group not in ['foo_resources', 'bar_resources']
|
||||
- location not in ['southcentralus', 'westus']
|
||||
fail_on_template_errors: false
|
||||
hostvar_expressions:
|
||||
ansible_host: private_ipv4_addresses[0]
|
||||
computer_name: name
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"ANSIBLE_JINJA2_NATIVE": "True",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||
"AWS_ACCESS_KEY_ID": "fooo",
|
||||
"AWS_SECRET_ACCESS_KEY": "fooo",
|
||||
"AWS_SECURITY_TOKEN": "fooo",
|
||||
"ANSIBLE_JINJA2_NATIVE": "True",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never"
|
||||
"AWS_SECURITY_TOKEN": "fooo"
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||
"GCE_CREDENTIALS_FILE_PATH": "{{ file_reference }}"
|
||||
"GCE_CREDENTIALS_FILE_PATH": "{{ file_reference }}",
|
||||
"GCP_AUTH_KIND": "serviceaccount",
|
||||
"GCP_ENV_TYPE": "tower",
|
||||
"GCP_PROJECT": "fooo",
|
||||
"GCP_SERVICE_ACCOUNT_FILE": "{{ file_reference }}"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"FOREMAN_SERVER": "https://foo.invalid",
|
||||
"FOREMAN_USER": "fooo",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||
"FOREMAN_PASSWORD": "fooo",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never"
|
||||
"FOREMAN_SERVER": "https://foo.invalid",
|
||||
"FOREMAN_USER": "fooo"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||
"TOWER_HOST": "https://foo.invalid",
|
||||
"TOWER_USERNAME": "fooo",
|
||||
"TOWER_PASSWORD": "fooo",
|
||||
"TOWER_VERIFY_SSL": "False",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never"
|
||||
"TOWER_USERNAME": "fooo",
|
||||
"TOWER_VERIFY_SSL": "False"
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"AZURE_SUBSCRIPTION_ID": "fooo",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||
"AZURE_CLIENT_ID": "fooo",
|
||||
"AZURE_TENANT": "fooo",
|
||||
"AZURE_SECRET": "fooo",
|
||||
"AZURE_CLOUD_ENVIRONMENT": "fooo",
|
||||
"AZURE_INI_PATH": "{{ file_reference }}",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never"
|
||||
"AZURE_SECRET": "fooo",
|
||||
"AZURE_SUBSCRIPTION_ID": "fooo",
|
||||
"AZURE_TENANT": "fooo"
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"CLOUDFORMS_INI_PATH": "{{ file_reference }}",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never"
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||
"CLOUDFORMS_INI_PATH": "{{ file_reference }}"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||
"AWS_ACCESS_KEY_ID": "fooo",
|
||||
"AWS_SECRET_ACCESS_KEY": "fooo",
|
||||
"AWS_SECURITY_TOKEN": "fooo",
|
||||
"EC2_INI_PATH": "{{ file_reference }}",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never"
|
||||
"EC2_INI_PATH": "{{ file_reference }}"
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"GCE_EMAIL": "fooo",
|
||||
"GCE_PROJECT": "fooo",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||
"GCE_CREDENTIALS_FILE_PATH": "{{ file_reference }}",
|
||||
"GCE_EMAIL": "fooo",
|
||||
"GCE_INI_PATH": "{{ file_reference_0 }}",
|
||||
"GCE_PROJECT": "fooo",
|
||||
"GCE_ZONE": "us-east4-a,us-west1-b",
|
||||
"GCE_INI_PATH": "{{ file_reference }}",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never"
|
||||
"GCP_AUTH_KIND": "serviceaccount",
|
||||
"GCP_ENV_TYPE": "tower",
|
||||
"GCP_PROJECT": "fooo",
|
||||
"GCP_SERVICE_ACCOUNT_FILE": "{{ file_reference }}"
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"OS_CLIENT_CONFIG_FILE": "{{ file_reference }}",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never"
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||
"OS_CLIENT_CONFIG_FILE": "{{ file_reference }}"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||
"OVIRT_INI_PATH": "{{ file_reference }}",
|
||||
"OVIRT_URL": "https://foo.invalid",
|
||||
"OVIRT_USERNAME": "fooo",
|
||||
"OVIRT_PASSWORD": "fooo",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never"
|
||||
"OVIRT_URL": "https://foo.invalid",
|
||||
"OVIRT_USERNAME": "fooo"
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"FOREMAN_INI_PATH": "{{ file_reference }}",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never"
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||
"FOREMAN_INI_PATH": "{{ file_reference }}"
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||
"TOWER_HOST": "https://foo.invalid",
|
||||
"TOWER_USERNAME": "fooo",
|
||||
"TOWER_PASSWORD": "fooo",
|
||||
"TOWER_VERIFY_SSL": "False",
|
||||
"TOWER_INVENTORY": "42",
|
||||
"TOWER_LICENSE_TYPE": "open",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never"
|
||||
"TOWER_PASSWORD": "fooo",
|
||||
"TOWER_USERNAME": "fooo",
|
||||
"TOWER_VERIFY_SSL": "False"
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"VMWARE_USER": "fooo",
|
||||
"VMWARE_PASSWORD": "fooo",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
|
||||
"VMWARE_HOST": "https://foo.invalid",
|
||||
"VMWARE_VALIDATE_CERTS": "False",
|
||||
"VMWARE_INI_PATH": "{{ file_reference }}",
|
||||
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never"
|
||||
"VMWARE_PASSWORD": "fooo",
|
||||
"VMWARE_USER": "fooo",
|
||||
"VMWARE_VALIDATE_CERTS": "False"
|
||||
}
|
||||
@@ -101,7 +101,7 @@ def test_instance_group_is_isolated(instance_group, isolated_instance_group):
|
||||
assert not instance_group.is_isolated
|
||||
assert isolated_instance_group.is_isolated
|
||||
|
||||
isolated_instance_group.instances = []
|
||||
isolated_instance_group.instances.set([])
|
||||
|
||||
assert isolated_instance_group.is_isolated
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ def run_command(name, *args, **options):
|
||||
command_runner = options.pop('command_runner', call_command)
|
||||
stdin_fileobj = options.pop('stdin_fileobj', None)
|
||||
options.setdefault('verbosity', 1)
|
||||
options.setdefault('interactive', False)
|
||||
original_stdin = sys.stdin
|
||||
original_stdout = sys.stdout
|
||||
original_stderr = sys.stderr
|
||||
|
||||
@@ -11,9 +11,9 @@ from django.urls import resolve
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.backends.sqlite3.base import SQLiteCursorWrapper
|
||||
from jsonbfield.fields import JSONField
|
||||
|
||||
# AWX
|
||||
from awx.main.fields import JSONBField
|
||||
from awx.main.models.projects import Project
|
||||
from awx.main.models.ha import Instance
|
||||
|
||||
@@ -737,7 +737,7 @@ def get_db_prep_save(self, value, connection, **kwargs):
|
||||
|
||||
@pytest.fixture
|
||||
def monkeypatch_jsonbfield_get_db_prep_save(mocker):
|
||||
JSONField.get_db_prep_save = get_db_prep_save
|
||||
JSONBField.get_db_prep_save = get_db_prep_save
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -457,4 +457,4 @@ def test_duplicate_name_within_template(job_template):
|
||||
with pytest.raises(IntegrityError) as ierror:
|
||||
s2.save()
|
||||
|
||||
assert str(ierror.value) == "columns unified_job_template_id, name are not unique"
|
||||
assert str(ierror.value) == "UNIQUE constraint failed: main_schedule.unified_job_template_id, main_schedule.name"
|
||||
|
||||
@@ -20,7 +20,6 @@ from django.test import TransactionTestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestWorkflowDAGFunctional(TransactionTestCase):
|
||||
def workflow_job(self, states=['new', 'new', 'new', 'new', 'new']):
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import pytest
|
||||
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from awx.main.models import (
|
||||
@@ -8,7 +6,6 @@ from awx.main.models import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestCapacityMapping(TransactionTestCase):
|
||||
|
||||
def sample_cluster(self):
|
||||
|
||||
@@ -129,6 +129,7 @@ def read_content(private_data_dir, raw_env, inventory_update):
|
||||
return a dictionary `content` with file contents, keyed off environment variable
|
||||
that references the file
|
||||
"""
|
||||
# build dict env as a mapping of environment variables to file names
|
||||
# Filter out environment variables which come from runtime environment
|
||||
env = {}
|
||||
exclude_keys = set(('PATH', 'INVENTORY_SOURCE_ID', 'INVENTORY_UPDATE_ID'))
|
||||
@@ -142,71 +143,74 @@ def read_content(private_data_dir, raw_env, inventory_update):
|
||||
env[k] = v
|
||||
inverse_env = {}
|
||||
for key, value in env.items():
|
||||
inverse_env[value] = key
|
||||
inverse_env.setdefault(value, []).append(key)
|
||||
|
||||
cache_file_regex = re.compile(r'/tmp/awx_{0}_[a-zA-Z0-9_]+/{1}_cache[a-zA-Z0-9_]+'.format(
|
||||
inventory_update.id, inventory_update.source)
|
||||
)
|
||||
private_key_regex = re.compile(r'-----BEGIN ENCRYPTED PRIVATE KEY-----.*-----END ENCRYPTED PRIVATE KEY-----')
|
||||
|
||||
# read directory content
|
||||
# build a mapping of the file paths to aliases which will be constant accross runs
|
||||
dir_contents = {}
|
||||
references = {}
|
||||
for filename in os.listdir(private_data_dir):
|
||||
referenced_paths = set()
|
||||
file_aliases = {}
|
||||
filename_list = sorted(os.listdir(private_data_dir), key=lambda fn: inverse_env.get(os.path.join(private_data_dir, fn), [fn])[0])
|
||||
for filename in filename_list:
|
||||
if filename in ('args', 'project'):
|
||||
continue # Ansible runner
|
||||
abs_file_path = os.path.join(private_data_dir, filename)
|
||||
file_aliases[abs_file_path] = filename
|
||||
if abs_file_path in inverse_env:
|
||||
env_key = inverse_env[abs_file_path]
|
||||
references[abs_file_path] = env_key
|
||||
env[env_key] = '{{ file_reference }}'
|
||||
referenced_paths.add(abs_file_path)
|
||||
alias = 'file_reference'
|
||||
for i in range(10):
|
||||
if alias not in file_aliases.values():
|
||||
break
|
||||
alias = 'file_reference_{}'.format(i)
|
||||
else:
|
||||
raise RuntimeError('Test not able to cope with >10 references by env vars. '
|
||||
'Something probably went very wrong.')
|
||||
file_aliases[abs_file_path] = alias
|
||||
for env_key in inverse_env[abs_file_path]:
|
||||
env[env_key] = '{{{{ {} }}}}'.format(alias)
|
||||
try:
|
||||
with open(abs_file_path, 'r') as f:
|
||||
dir_contents[abs_file_path] = f.read()
|
||||
# Declare a reference to inventory plugin file if it exists
|
||||
if abs_file_path.endswith('.yml') and 'plugin: ' in dir_contents[abs_file_path]:
|
||||
references[abs_file_path] = filename # plugin filenames are universal
|
||||
referenced_paths.add(abs_file_path) # used as inventory file
|
||||
elif cache_file_regex.match(abs_file_path):
|
||||
file_aliases[abs_file_path] = 'cache_file'
|
||||
except IsADirectoryError:
|
||||
dir_contents[abs_file_path] = '<directory>'
|
||||
if cache_file_regex.match(abs_file_path):
|
||||
file_aliases[abs_file_path] = 'cache_dir'
|
||||
|
||||
# Declare cross-file references, also use special keywords if it is the cache
|
||||
cache_referenced = False
|
||||
cache_present = False
|
||||
# Substitute in aliases for cross-file references
|
||||
for abs_file_path, file_content in dir_contents.copy().items():
|
||||
if cache_file_regex.match(file_content):
|
||||
cache_referenced = True
|
||||
if 'cache_dir' not in file_aliases.values() and 'cache_file' not in file_aliases in file_aliases.values():
|
||||
raise AssertionError(
|
||||
'A cache file was referenced but never created, files:\n{}'.format(
|
||||
json.dumps(dir_contents, indent=4)))
|
||||
# if another files path appears in this file, replace it with its alias
|
||||
for target_path in dir_contents.keys():
|
||||
other_alias = file_aliases[target_path]
|
||||
if target_path in file_content:
|
||||
if target_path in references:
|
||||
raise AssertionError(
|
||||
'File {} is referenced by env var or other file as well as file {}:\n{}\n{}'.format(
|
||||
target_path, abs_file_path, json.dumps(env, indent=4), json.dumps(dir_contents, indent=4)))
|
||||
else:
|
||||
if cache_file_regex.match(target_path):
|
||||
cache_present = True
|
||||
if os.path.isdir(target_path):
|
||||
keyword = 'cache_dir'
|
||||
else:
|
||||
keyword = 'cache_file'
|
||||
references[target_path] = keyword
|
||||
new_file_content = cache_file_regex.sub('{{ ' + keyword + ' }}', file_content)
|
||||
else:
|
||||
references[target_path] = 'file_reference'
|
||||
new_file_content = file_content.replace(target_path, '{{ file_reference }}')
|
||||
dir_contents[abs_file_path] = new_file_content
|
||||
if cache_referenced and not cache_present:
|
||||
raise AssertionError(
|
||||
'A cache file was referenced but never created, files:\n{}'.format(
|
||||
json.dumps(dir_contents, indent=4)))
|
||||
referenced_paths.add(target_path)
|
||||
dir_contents[abs_file_path] = file_content.replace(target_path, '{{ ' + other_alias + ' }}')
|
||||
|
||||
# build dict content which is the directory contents keyed off the file aliases
|
||||
content = {}
|
||||
for abs_file_path, file_content in dir_contents.items():
|
||||
if abs_file_path not in references:
|
||||
# assert that all files laid down are used
|
||||
if abs_file_path not in referenced_paths:
|
||||
raise AssertionError(
|
||||
"File {} is not referenced. References and files:\n{}\n{}".format(
|
||||
abs_file_path, json.dumps(references, indent=4), json.dumps(dir_contents, indent=4)))
|
||||
reference_key = references[abs_file_path]
|
||||
abs_file_path, json.dumps(env, indent=4), json.dumps(dir_contents, indent=4)))
|
||||
file_content = private_key_regex.sub('{{private_key}}', file_content)
|
||||
content[reference_key] = file_content
|
||||
content[file_aliases[abs_file_path]] = file_content
|
||||
|
||||
return (env, content)
|
||||
|
||||
@@ -223,7 +227,7 @@ def create_reference_data(source_dir, env, content):
|
||||
f.write(content)
|
||||
if env:
|
||||
with open(os.path.join(source_dir, 'env.json'), 'w') as f:
|
||||
f.write(json.dumps(env, indent=4))
|
||||
json.dump(env, f, indent=4, sort_keys=True)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -258,6 +262,7 @@ def test_inventory_update_injected_content(this_kind, script_or_plugin, inventor
|
||||
"""
|
||||
private_data_dir = envvars.pop('AWX_PRIVATE_DATA_DIR')
|
||||
assert envvars.pop('ANSIBLE_INVENTORY_ENABLED') == ('auto' if use_plugin else 'script')
|
||||
assert envvars.pop('ANSIBLE_COLLECTIONS_PATHS') == os.pathsep.join(settings.AWX_ANSIBLE_COLLECTIONS_PATHS)
|
||||
set_files = bool(os.getenv("MAKE_INVENTORY_REFERENCE_FILES", 'false').lower()[0] not in ['f', '0'])
|
||||
env, content = read_content(private_data_dir, envvars, inventory_update)
|
||||
base_dir = os.path.join(DATA, script_or_plugin)
|
||||
@@ -283,7 +288,7 @@ def test_inventory_update_injected_content(this_kind, script_or_plugin, inventor
|
||||
for f_name in expected_file_list:
|
||||
with open(os.path.join(files_dir, f_name), 'r') as f:
|
||||
ref_content = f.read()
|
||||
assert ref_content == content[f_name]
|
||||
assert ref_content == content[f_name], f_name
|
||||
try:
|
||||
with open(os.path.join(source_dir, 'env.json'), 'r') as f:
|
||||
ref_env_text = f.read()
|
||||
|
||||
@@ -42,7 +42,7 @@ def test_job_notification_data(inventory, machine_credential, project):
|
||||
survey_passwords={"SSN": encrypted_str},
|
||||
project=project,
|
||||
)
|
||||
job.credentials = [machine_credential]
|
||||
job.credentials.set([machine_credential])
|
||||
notification_data = job.notification_data(block=0)
|
||||
assert json.loads(notification_data['extra_vars'])['SSN'] == encrypted_str
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from awx.main.access import UserAccess, RoleAccess, TeamAccess
|
||||
from awx.main.models import User, Organization, Inventory
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSysAuditorTransactional(TransactionTestCase):
|
||||
def rando(self):
|
||||
return User.objects.create(username='rando', password='rando', email='rando@com.com')
|
||||
|
||||
@@ -186,11 +186,8 @@ class TestResourceAccessList:
|
||||
|
||||
def mock_request(self):
|
||||
return mock.MagicMock(
|
||||
user=mock.MagicMock(
|
||||
is_anonymous=mock.MagicMock(return_value=False),
|
||||
is_superuser=False
|
||||
), method='GET')
|
||||
|
||||
user=mock.MagicMock(is_anonymous=False, is_superuser=False),
|
||||
method='GET')
|
||||
|
||||
def mock_view(self, parent=None):
|
||||
view = ResourceAccessList()
|
||||
@@ -200,7 +197,6 @@ class TestResourceAccessList:
|
||||
view.get_parent_object = lambda: parent
|
||||
return view
|
||||
|
||||
|
||||
def test_parent_access_check_failed(self, mocker, mock_organization):
|
||||
mock_access = mocker.MagicMock(__name__='for logger', return_value=False)
|
||||
with mocker.patch('awx.main.access.BaseAccess.can_read', mock_access):
|
||||
@@ -208,7 +204,6 @@ class TestResourceAccessList:
|
||||
self.mock_view(parent=mock_organization).check_permissions(self.mock_request())
|
||||
mock_access.assert_called_once_with(mock_organization)
|
||||
|
||||
|
||||
def test_parent_access_check_worked(self, mocker, mock_organization):
|
||||
mock_access = mocker.MagicMock(__name__='for logger', return_value=True)
|
||||
with mocker.patch('awx.main.access.BaseAccess.can_read', mock_access):
|
||||
|
||||
@@ -6,7 +6,7 @@ from unittest.mock import PropertyMock
|
||||
from awx.api.urls import urlpatterns as api_patterns
|
||||
|
||||
# Django
|
||||
from django.urls import RegexURLResolver, RegexURLPattern
|
||||
from django.urls import URLResolver, URLPattern
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -20,24 +20,24 @@ def all_views():
|
||||
'''
|
||||
returns a set of all views in the app
|
||||
'''
|
||||
patterns = set([])
|
||||
url_views = set([])
|
||||
patterns = set()
|
||||
url_views = set()
|
||||
# Add recursive URL patterns
|
||||
unprocessed = set(api_patterns)
|
||||
while unprocessed:
|
||||
to_process = unprocessed.copy()
|
||||
unprocessed = set([])
|
||||
unprocessed = set()
|
||||
for pattern in to_process:
|
||||
if hasattr(pattern, 'lookup_str') and not pattern.lookup_str.startswith('awx.api'):
|
||||
continue
|
||||
patterns.add(pattern)
|
||||
if isinstance(pattern, RegexURLResolver):
|
||||
if isinstance(pattern, URLResolver):
|
||||
for sub_pattern in pattern.url_patterns:
|
||||
if sub_pattern not in patterns:
|
||||
unprocessed.add(sub_pattern)
|
||||
# Get view classes
|
||||
for pattern in patterns:
|
||||
if isinstance(pattern, RegexURLPattern) and hasattr(pattern.callback, 'view_class'):
|
||||
if isinstance(pattern, URLPattern) and hasattr(pattern.callback, 'view_class'):
|
||||
url_views.add(pattern.callback.view_class)
|
||||
return url_views
|
||||
|
||||
|
||||
@@ -57,5 +57,5 @@ def test_really_long_event_fields(field):
|
||||
})
|
||||
manager.create.assert_called_with(**{
|
||||
'job_id': 123,
|
||||
'event_data': {field: 'X' * 1021 + '...'}
|
||||
'event_data': {field: 'X' * 1023 + '…'}
|
||||
})
|
||||
|
||||
@@ -9,7 +9,8 @@ from awx.main.models import (
|
||||
Job,
|
||||
User,
|
||||
Project,
|
||||
JobTemplate
|
||||
JobTemplate,
|
||||
Inventory
|
||||
)
|
||||
|
||||
|
||||
@@ -81,11 +82,13 @@ class TestMetaVars:
|
||||
|
||||
def test_job_metavars(self):
|
||||
maker = User(username='joe', pk=47, id=47)
|
||||
inv = Inventory(name='example-inv', id=45)
|
||||
assert Job(
|
||||
name='fake-job',
|
||||
pk=42, id=42,
|
||||
launch_type='manual',
|
||||
created_by=maker
|
||||
created_by=maker,
|
||||
inventory=inv
|
||||
).awx_meta_vars() == {
|
||||
'tower_job_id': 42,
|
||||
'awx_job_id': 42,
|
||||
@@ -100,7 +103,11 @@ class TestMetaVars:
|
||||
'awx_user_last_name': '',
|
||||
'tower_user_last_name': '',
|
||||
'awx_user_id': 47,
|
||||
'tower_user_id': 47
|
||||
'tower_user_id': 47,
|
||||
'tower_inventory_id': 45,
|
||||
'awx_inventory_id': 45,
|
||||
'tower_inventory_name': 'example-inv',
|
||||
'awx_inventory_name': 'example-inv'
|
||||
}
|
||||
|
||||
def test_project_update_metavars(self):
|
||||
|
||||
@@ -256,6 +256,7 @@ class TestExtraVarSanitation(TestJobExecution):
|
||||
|
||||
def test_vars_unsafe_by_default(self, job, private_data_dir):
|
||||
job.created_by = User(pk=123, username='angry-spud')
|
||||
job.inventory = Inventory(pk=123, name='example-inv')
|
||||
|
||||
task = tasks.RunJob()
|
||||
task.build_extra_vars_file(job, private_data_dir)
|
||||
@@ -268,13 +269,14 @@ class TestExtraVarSanitation(TestJobExecution):
|
||||
'awx_user_name', 'tower_job_launch_type',
|
||||
'awx_project_revision',
|
||||
'tower_project_revision', 'tower_user_name',
|
||||
'awx_job_launch_type']:
|
||||
'awx_job_launch_type',
|
||||
'awx_inventory_name', 'tower_inventory_name']:
|
||||
assert hasattr(extra_vars[unsafe], '__UNSAFE__')
|
||||
|
||||
# ensure that non-strings are marked as safe
|
||||
for safe in ['awx_job_template_id', 'awx_job_id', 'awx_user_id',
|
||||
'tower_user_id', 'tower_job_template_id',
|
||||
'tower_job_id']:
|
||||
'tower_job_id', 'awx_inventory_id', 'tower_inventory_id']:
|
||||
assert not hasattr(extra_vars[safe], '__UNSAFE__')
|
||||
|
||||
|
||||
@@ -439,6 +441,7 @@ class TestGenericRun():
|
||||
settings.AWX_PROOT_HIDE_PATHS = ['/AWX_PROOT_HIDE_PATHS1', '/AWX_PROOT_HIDE_PATHS2']
|
||||
settings.ANSIBLE_VENV_PATH = '/ANSIBLE_VENV_PATH'
|
||||
settings.AWX_VENV_PATH = '/AWX_VENV_PATH'
|
||||
settings.AWX_ANSIBLE_COLLECTIONS_PATHS = ['/AWX_COLLECTION_PATH1', '/AWX_COLLECTION_PATH2']
|
||||
|
||||
process_isolation_params = task.build_params_process_isolation(job, private_data_dir, cwd)
|
||||
assert True is process_isolation_params['process_isolation']
|
||||
@@ -448,6 +451,10 @@ class TestGenericRun():
|
||||
"The per-job private data dir should be in the list of directories the user can see."
|
||||
assert cwd in process_isolation_params['process_isolation_show_paths'], \
|
||||
"The current working directory should be in the list of directories the user can see."
|
||||
assert '/AWX_COLLECTION_PATH1' in process_isolation_params['process_isolation_show_paths'], \
|
||||
"AWX global collection directory 1 of 2 should get added to the list of directories the user can see."
|
||||
assert '/AWX_COLLECTION_PATH2' in process_isolation_params['process_isolation_show_paths'], \
|
||||
"AWX global collection directory 2 of 2 should get added to the list of directories the user can see."
|
||||
|
||||
for p in [settings.AWX_PROOT_BASE_PATH,
|
||||
'/etc/tower',
|
||||
@@ -507,6 +514,17 @@ class TestGenericRun():
|
||||
env = task.build_env(job, private_data_dir)
|
||||
assert env['FOO'] == 'BAR'
|
||||
|
||||
def test_awx_task_env_respects_ansible_collections_paths(self, patch_Job, private_data_dir):
|
||||
job = Job(project=Project(), inventory=Inventory())
|
||||
|
||||
task = tasks.RunJob()
|
||||
task._write_extra_vars_file = mock.Mock()
|
||||
|
||||
with mock.patch('awx.main.tasks.settings.AWX_ANSIBLE_COLLECTIONS_PATHS', ['/AWX_COLLECTION_PATH']):
|
||||
with mock.patch('awx.main.tasks.settings.AWX_TASK_ENV', {'ANSIBLE_COLLECTIONS_PATHS': '/MY_COLLECTION1:/MY_COLLECTION2'}):
|
||||
env = task.build_env(job, private_data_dir)
|
||||
assert env['ANSIBLE_COLLECTIONS_PATHS'] == '/MY_COLLECTION1:/MY_COLLECTION2:/AWX_COLLECTION_PATH'
|
||||
|
||||
def test_valid_custom_virtualenv(self, patch_Job, private_data_dir):
|
||||
job = Job(project=Project(), inventory=Inventory())
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ __all__ = ['get_object_or_400', 'camelcase_to_underscore', 'underscore_to_camelc
|
||||
'wrap_args_with_proot', 'build_proot_temp_dir', 'check_proot_installed', 'model_to_dict',
|
||||
'model_instance_diff', 'parse_yaml_or_json', 'RequireDebugTrueOrTest',
|
||||
'has_model_field_prefetched', 'set_environ', 'IllegalArgumentError', 'get_custom_venv_choices', 'get_external_account',
|
||||
'task_manager_bulk_reschedule', 'schedule_task_manager', 'classproperty']
|
||||
'task_manager_bulk_reschedule', 'schedule_task_manager', 'classproperty', 'create_temporary_fifo']
|
||||
|
||||
|
||||
def get_object_or_400(klass, *args, **kwargs):
|
||||
@@ -1015,3 +1015,20 @@ class classproperty:
|
||||
|
||||
def __get__(self, instance, ownerclass):
|
||||
return self.fget(ownerclass)
|
||||
|
||||
|
||||
def create_temporary_fifo(data):
|
||||
"""Open fifo named pipe in a new thread using a temporary file path. The
|
||||
thread blocks until data is read from the pipe.
|
||||
Returns the path to the fifo.
|
||||
:param data(bytes): Data to write to the pipe.
|
||||
"""
|
||||
path = os.path.join(tempfile.mkdtemp(), next(tempfile._get_candidate_names()))
|
||||
os.mkfifo(path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
|
||||
threading.Thread(
|
||||
target=lambda p, d: open(p, 'wb').write(d),
|
||||
args=(path, data)
|
||||
).start()
|
||||
return path
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ def _check_unique_together_fields(model, ut):
|
||||
field = model._meta.get_field(field_name)
|
||||
if field_name == 'name':
|
||||
has_name = True
|
||||
elif type(field) == models.ForeignKey and field.rel.to != model:
|
||||
elif type(field) == models.ForeignKey and field.related_model != model:
|
||||
fk_names.append(field_name)
|
||||
elif issubclass(type(field), models.CharField) and field.choices:
|
||||
fields.append(field_name)
|
||||
@@ -256,7 +256,7 @@ def _dfs(configuration, model, graph, dead_ends, new_deadends, parents):
|
||||
fields, fk_names = configuration[model][0][:], configuration[model][1][:]
|
||||
adj_list = []
|
||||
for fk_name in fk_names:
|
||||
next_model = model._meta.get_field(fk_name).rel.to
|
||||
next_model = model._meta.get_field(fk_name).related_model
|
||||
if issubclass(next_model, ContentType):
|
||||
continue
|
||||
if next_model not in configuration or\
|
||||
|
||||
@@ -60,7 +60,7 @@ def handle_error(request, status=404, **kwargs):
|
||||
return render(request, 'error.html', kwargs, status=status)
|
||||
|
||||
|
||||
def handle_400(request):
|
||||
def handle_400(request, exception):
|
||||
kwargs = {
|
||||
'name': _('Bad Request'),
|
||||
'content': _('The request could not be understood by the server.'),
|
||||
@@ -68,7 +68,7 @@ def handle_400(request):
|
||||
return handle_error(request, 400, **kwargs)
|
||||
|
||||
|
||||
def handle_403(request):
|
||||
def handle_403(request, exception):
|
||||
kwargs = {
|
||||
'name': _('Forbidden'),
|
||||
'content': _('You don\'t have permission to access the requested resource.'),
|
||||
@@ -76,7 +76,7 @@ def handle_403(request):
|
||||
return handle_error(request, 403, **kwargs)
|
||||
|
||||
|
||||
def handle_404(request):
|
||||
def handle_404(request, exception):
|
||||
kwargs = {
|
||||
'name': _('Not Found'),
|
||||
'content': _('The requested resource could not be found.'),
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
args:
|
||||
chdir: "{{project_path|quote}}/roles"
|
||||
register: galaxy_result
|
||||
when: doesRequirementsExist.stat.exists and (scm_version is undefined or (git_result is defined and git_result['before'] == git_result['after']))
|
||||
when: doesRequirementsExist.stat.exists and (scm_version is undefined or (git_result is not skipped and git_result['before'] == git_result['after']))
|
||||
changed_when: "'was installed successfully' in galaxy_result.stdout"
|
||||
|
||||
- name: fetch galaxy roles from requirements.yml (forced update)
|
||||
|
||||
@@ -1187,6 +1187,9 @@ AWX_REQUEST_PROFILE = False
|
||||
# Delete temporary directories created to store playbook run-time
|
||||
AWX_CLEANUP_PATHS = True
|
||||
|
||||
# Expose collections to Ansible playbooks
|
||||
AWX_ANSIBLE_COLLECTIONS_PATHS = ['/var/lib/awx/collections']
|
||||
|
||||
MIDDLEWARE = [
|
||||
'awx.main.middleware.TimingMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
|
||||
@@ -276,3 +276,5 @@ TEST_OPENSTACK_PROJECT = ''
|
||||
# Azure credentials.
|
||||
TEST_AZURE_USERNAME = ''
|
||||
TEST_AZURE_KEY_DATA = ''
|
||||
|
||||
AWX_ANSIBLE_COLLECTIONS_PATHS = ['/tmp/collections']
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.dispatch import receiver
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings as django_settings
|
||||
from django.core.signals import setting_changed
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
# django-auth-ldap
|
||||
from django_auth_ldap.backend import LDAPSettings as BaseLDAPSettings
|
||||
@@ -66,7 +67,7 @@ class LDAPSettings(BaseLDAPSettings):
|
||||
# see: https://github.com/python-ldap/python-ldap/issues/55
|
||||
newctx_option = self.CONNECTION_OPTIONS.pop(ldap.OPT_X_TLS_NEWCTX, None)
|
||||
self.CONNECTION_OPTIONS = OrderedDict(self.CONNECTION_OPTIONS)
|
||||
if newctx_option:
|
||||
if newctx_option is not None:
|
||||
self.CONNECTION_OPTIONS[ldap.OPT_X_TLS_NEWCTX] = newctx_option
|
||||
|
||||
|
||||
@@ -98,7 +99,7 @@ class LDAPBackend(BaseLDAPBackend):
|
||||
|
||||
settings = property(_get_settings, _set_settings)
|
||||
|
||||
def authenticate(self, username, password):
|
||||
def authenticate(self, request, username, password):
|
||||
if self.settings.START_TLS and ldap.OPT_X_TLS_REQUIRE_CERT in self.settings.CONNECTION_OPTIONS:
|
||||
# with python-ldap, if you want to set connection-specific TLS
|
||||
# parameters, you must also specify OPT_X_TLS_NEWCTX = 0
|
||||
@@ -124,7 +125,7 @@ class LDAPBackend(BaseLDAPBackend):
|
||||
raise ImproperlyConfigured(
|
||||
"{} must be an {} instance.".format(setting_name, type_)
|
||||
)
|
||||
return super(LDAPBackend, self).authenticate(None, username, password)
|
||||
return super(LDAPBackend, self).authenticate(request, username, password)
|
||||
except Exception:
|
||||
logger.exception("Encountered an error authenticating to LDAP")
|
||||
return None
|
||||
@@ -179,7 +180,7 @@ def _decorate_enterprise_user(user, provider):
|
||||
def _get_or_set_enterprise_user(username, password, provider):
|
||||
created = False
|
||||
try:
|
||||
user = User.objects.all().prefetch_related('enterprise_auth').get(username=username)
|
||||
user = User.objects.prefetch_related('enterprise_auth').get(username=username)
|
||||
except User.DoesNotExist:
|
||||
user = User(username=username)
|
||||
enterprise_auth = _decorate_enterprise_user(user, provider)
|
||||
@@ -196,10 +197,10 @@ class RADIUSBackend(BaseRADIUSBackend):
|
||||
Custom Radius backend to verify license status
|
||||
'''
|
||||
|
||||
def authenticate(self, username, password):
|
||||
def authenticate(self, request, username, password):
|
||||
if not django_settings.RADIUS_SERVER:
|
||||
return None
|
||||
return super(RADIUSBackend, self).authenticate(None, username, password)
|
||||
return super(RADIUSBackend, self).authenticate(request, username, password)
|
||||
|
||||
def get_user(self, user_id):
|
||||
if not django_settings.RADIUS_SERVER:
|
||||
@@ -209,7 +210,7 @@ class RADIUSBackend(BaseRADIUSBackend):
|
||||
return user
|
||||
|
||||
def get_django_user(self, username, password=None):
|
||||
return _get_or_set_enterprise_user(username, password, 'radius')
|
||||
return _get_or_set_enterprise_user(force_text(username), force_text(password), 'radius')
|
||||
|
||||
|
||||
class TACACSPlusBackend(object):
|
||||
@@ -217,7 +218,7 @@ class TACACSPlusBackend(object):
|
||||
Custom TACACS+ auth backend for AWX
|
||||
'''
|
||||
|
||||
def authenticate(self, username, password):
|
||||
def authenticate(self, request, username, password):
|
||||
if not django_settings.TACACSPLUS_HOST:
|
||||
return None
|
||||
try:
|
||||
@@ -284,13 +285,13 @@ class SAMLAuth(BaseSAMLAuth):
|
||||
idp_config = self.setting('ENABLED_IDPS')[idp_name]
|
||||
return TowerSAMLIdentityProvider(idp_name, **idp_config)
|
||||
|
||||
def authenticate(self, *args, **kwargs):
|
||||
def authenticate(self, request, *args, **kwargs):
|
||||
if not all([django_settings.SOCIAL_AUTH_SAML_SP_ENTITY_ID, django_settings.SOCIAL_AUTH_SAML_SP_PUBLIC_CERT,
|
||||
django_settings.SOCIAL_AUTH_SAML_SP_PRIVATE_KEY, django_settings.SOCIAL_AUTH_SAML_ORG_INFO,
|
||||
django_settings.SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, django_settings.SOCIAL_AUTH_SAML_SUPPORT_CONTACT,
|
||||
django_settings.SOCIAL_AUTH_SAML_ENABLED_IDPS]):
|
||||
return None
|
||||
user = super(SAMLAuth, self).authenticate(*args, **kwargs)
|
||||
user = super(SAMLAuth, self).authenticate(request, *args, **kwargs)
|
||||
# Comes from https://github.com/omab/python-social-auth/blob/v0.2.21/social/backends/base.py#L91
|
||||
if getattr(user, 'is_new', False):
|
||||
_decorate_enterprise_user(user, 'saml')
|
||||
@@ -307,7 +308,7 @@ class SAMLAuth(BaseSAMLAuth):
|
||||
return super(SAMLAuth, self).get_user(user_id)
|
||||
|
||||
|
||||
def _update_m2m_from_groups(user, ldap_user, rel, opts, remove=True):
|
||||
def _update_m2m_from_groups(user, ldap_user, related, opts, remove=True):
|
||||
'''
|
||||
Hepler function to update m2m relationship based on LDAP group membership.
|
||||
'''
|
||||
@@ -328,10 +329,10 @@ def _update_m2m_from_groups(user, ldap_user, rel, opts, remove=True):
|
||||
should_add = True
|
||||
if should_add:
|
||||
user.save()
|
||||
rel.add(user)
|
||||
elif remove and user in rel.all():
|
||||
related.add(user)
|
||||
elif remove and user in related.all():
|
||||
user.save()
|
||||
rel.remove(user)
|
||||
related.remove(user)
|
||||
|
||||
|
||||
@receiver(populate_user, dispatch_uid='populate-ldap-user')
|
||||
|
||||
@@ -93,12 +93,14 @@ class HybridDictField(fields.DictField):
|
||||
self.allow_blank = kwargs.pop('allow_blank', False)
|
||||
|
||||
fields = [
|
||||
(field_name, obj)
|
||||
for field_name, obj in self.__class__.__dict__.items()
|
||||
if isinstance(obj, Field) and field_name != 'child'
|
||||
sorted(
|
||||
((field_name, obj) for field_name, obj in cls.__dict__.items()
|
||||
if isinstance(obj, Field) and field_name != 'child'),
|
||||
key=lambda x: x[1]._creation_counter
|
||||
)
|
||||
for cls in reversed(self.__class__.__mro__)
|
||||
]
|
||||
fields.sort(key=lambda x: x[1]._creation_counter)
|
||||
self._declared_fields = collections.OrderedDict(fields)
|
||||
self._declared_fields = collections.OrderedDict(f for group in fields for f in group)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class SocialAuthMiddleware(SocialAuthExceptionMiddleware):
|
||||
request.successful_authenticator = None
|
||||
|
||||
if not request.path.startswith('/sso/') and 'migrations_notran' not in request.path:
|
||||
if request.user and request.user.is_authenticated():
|
||||
if request.user and request.user.is_authenticated:
|
||||
# The rest of the code base rely hevily on type/inheritance checks,
|
||||
# LazyObject sent from Django auth middleware can be buggy if not
|
||||
# converted back to its original object.
|
||||
|
||||
@@ -17,7 +17,7 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('provider', models.CharField(max_length=32, choices=[(b'radius', 'RADIUS'), (b'tacacs+', 'TACACS+')])),
|
||||
('user', models.ForeignKey(related_name='enterprise_auth', to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.ForeignKey(related_name='enterprise_auth', on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
|
||||
@@ -50,7 +50,7 @@ def prevent_inactive_login(backend, details, user=None, *args, **kwargs):
|
||||
raise AuthInactive(backend)
|
||||
|
||||
|
||||
def _update_m2m_from_expression(user, rel, expr, remove=True):
|
||||
def _update_m2m_from_expression(user, related, expr, remove=True):
|
||||
'''
|
||||
Helper function to update m2m relationship based on user matching one or
|
||||
more expressions.
|
||||
@@ -73,12 +73,12 @@ def _update_m2m_from_expression(user, rel, expr, remove=True):
|
||||
if ex.match(user.username) or ex.match(user.email):
|
||||
should_add = True
|
||||
if should_add:
|
||||
rel.add(user)
|
||||
related.add(user)
|
||||
elif remove:
|
||||
rel.remove(user)
|
||||
related.remove(user)
|
||||
|
||||
|
||||
def _update_org_from_attr(user, rel, attr, remove, remove_admins):
|
||||
def _update_org_from_attr(user, related, attr, remove, remove_admins):
|
||||
from awx.main.models import Organization
|
||||
|
||||
org_ids = []
|
||||
@@ -87,7 +87,7 @@ def _update_org_from_attr(user, rel, attr, remove, remove_admins):
|
||||
org = Organization.objects.get_or_create(name=org_name)[0]
|
||||
|
||||
org_ids.append(org.id)
|
||||
getattr(org, rel).members.add(user)
|
||||
getattr(org, related).members.add(user)
|
||||
|
||||
if remove:
|
||||
[o.member_role.members.remove(user) for o in
|
||||
|
||||
@@ -27,6 +27,7 @@ def existing_tacacsplus_user():
|
||||
user = User.objects.get(username="foo")
|
||||
except User.DoesNotExist:
|
||||
user = User(username="foo")
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
enterprise_auth = UserEnterpriseAuth(user=user, provider='tacacs+')
|
||||
enterprise_auth.save()
|
||||
|
||||
@@ -8,8 +8,11 @@ from awx.sso.backends import _get_or_set_enterprise_user
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_fetch_user_if_exist(existing_tacacsplus_user):
|
||||
new_user = _get_or_set_enterprise_user("foo", "password", "tacacs+")
|
||||
assert new_user == existing_tacacsplus_user
|
||||
with mock.patch('awx.sso.backends.logger') as mocked_logger:
|
||||
new_user = _get_or_set_enterprise_user("foo", "password", "tacacs+")
|
||||
mocked_logger.debug.assert_not_called()
|
||||
mocked_logger.warn.assert_not_called()
|
||||
assert new_user == existing_tacacsplus_user
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -4,7 +4,7 @@ from unittest import mock
|
||||
def test_empty_host_fails_auth(tacacsplus_backend):
|
||||
with mock.patch('awx.sso.backends.django_settings') as settings:
|
||||
settings.TACACSPLUS_HOST = ''
|
||||
ret_user = tacacsplus_backend.authenticate(u"user", u"pass")
|
||||
ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass")
|
||||
assert ret_user is None
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ def test_client_raises_exception(tacacsplus_backend):
|
||||
mock.patch('tacacs_plus.TACACSClient', return_value=client):
|
||||
settings.TACACSPLUS_HOST = 'localhost'
|
||||
settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii'
|
||||
ret_user = tacacsplus_backend.authenticate(u"user", u"pass")
|
||||
ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass")
|
||||
assert ret_user is None
|
||||
logger.exception.assert_called_once_with(
|
||||
"TACACS+ Authentication Error: foo"
|
||||
@@ -32,7 +32,7 @@ def test_client_return_invalid_fails_auth(tacacsplus_backend):
|
||||
mock.patch('tacacs_plus.TACACSClient', return_value=client):
|
||||
settings.TACACSPLUS_HOST = 'localhost'
|
||||
settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii'
|
||||
ret_user = tacacsplus_backend.authenticate(u"user", u"pass")
|
||||
ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass")
|
||||
assert ret_user is None
|
||||
|
||||
|
||||
@@ -48,5 +48,5 @@ def test_client_return_valid_passes_auth(tacacsplus_backend):
|
||||
mock.patch('awx.sso.backends._get_or_set_enterprise_user', return_value=user):
|
||||
settings.TACACSPLUS_HOST = 'localhost'
|
||||
settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii'
|
||||
ret_user = tacacsplus_backend.authenticate(u"user", u"pass")
|
||||
ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass")
|
||||
assert ret_user == user
|
||||
|
||||
@@ -40,7 +40,7 @@ class CompleteView(BaseRedirectView):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super(CompleteView, self).dispatch(request, *args, **kwargs)
|
||||
if self.request.user and self.request.user.is_authenticated():
|
||||
if self.request.user and self.request.user.is_authenticated:
|
||||
logger.info(smart_text(u"User {} logged in".format(self.request.user.username)))
|
||||
response.set_cookie('userLoggedIn', 'true')
|
||||
current_user = UserSerializer(self.request.user)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "rest_framework/api.html" %}
|
||||
{% load i18n staticfiles %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block title %}{{ name }} · {% trans 'AWX' %}{% endblock %}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{# Partial copy of login_base.html from rest_framework with AWX change. #}
|
||||
{% extends 'rest_framework/api.html' %}
|
||||
{% load i18n staticfiles %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -422,10 +422,18 @@ function menuLast () {
|
||||
|
||||
function down () {
|
||||
scroll.moveDown();
|
||||
|
||||
if (scroll.isBeyondLowerThreshold()) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
function up () {
|
||||
scroll.moveUp();
|
||||
|
||||
if (scroll.isBeyondUpperThreshold()) {
|
||||
previous();
|
||||
}
|
||||
}
|
||||
|
||||
function togglePanelExpand () {
|
||||
|
||||
@@ -17,15 +17,12 @@ function OutputStrings (BaseString) {
|
||||
ARTIFACTS: t.s('Read-only view of artifacts added to the job template'),
|
||||
CANCEL: t.s('Cancel'),
|
||||
COLLAPSE_OUTPUT: t.s('Collapse Output'),
|
||||
CREDENTIAL: t.s('View the Credential'),
|
||||
DETAILS: t.s('Click for details'),
|
||||
DELETE: t.s('Delete'),
|
||||
DOWNLOAD_OUTPUT: t.s('Download Output'),
|
||||
EVENT_ID: t.s('Event ID:'),
|
||||
CREDENTIAL: t.s('View the Credential'),
|
||||
EXPAND_OUTPUT: t.s('Expand Output'),
|
||||
EXTRA_VARS: t.s('Read-only view of extra variables added to the job template'),
|
||||
HOST_LIMIT: t.s('When this field is true, the job\'s inventory belongs to an organization that has exceeded it\'s limit of hosts as defined by the system administrator.'),
|
||||
HOST_STATUS: t.s('Status:'),
|
||||
INVENTORY: t.s('View the Inventory'),
|
||||
INVENTORY_SCM: t.s('View the Project'),
|
||||
INVENTORY_SCM_JOB: t.s('View Project checkout results'),
|
||||
|
||||
@@ -35,7 +35,7 @@ const hasAnsi = input => re.test(input);
|
||||
|
||||
let $scope;
|
||||
|
||||
function JobRenderService ($q, $compile, $sce, $window, strings) {
|
||||
function JobRenderService ($q, $compile, $sce, $window) {
|
||||
this.init = (_$scope_, { toggles }) => {
|
||||
$scope = _$scope_;
|
||||
this.setScope();
|
||||
@@ -132,7 +132,7 @@ function JobRenderService ($q, $compile, $sce, $window, strings) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
const html = this.buildRowHTML(this.records[uuid], null, null, event);
|
||||
const html = this.buildRowHTML(this.records[uuid]);
|
||||
const count = 1;
|
||||
|
||||
return { html, count };
|
||||
@@ -193,7 +193,7 @@ function JobRenderService ($q, $compile, $sce, $window, strings) {
|
||||
return { html: '', count: 0 };
|
||||
}
|
||||
|
||||
const html = this.buildRowHTML(this.records[uuid], null, null, event);
|
||||
const html = this.buildRowHTML(this.records[uuid]);
|
||||
const count = 1;
|
||||
|
||||
return { html, count };
|
||||
@@ -226,10 +226,10 @@ function JobRenderService ($q, $compile, $sce, $window, strings) {
|
||||
const line = lines[i];
|
||||
const isLastLine = i === lines.length - 1;
|
||||
|
||||
let row = this.buildRowHTML(record, ln, line, event);
|
||||
let row = this.buildRowHTML(record, ln, line);
|
||||
|
||||
if (record && record.isTruncated && isLastLine) {
|
||||
row += this.buildRowHTML(record, null, null, event);
|
||||
row += this.buildRowHTML(record);
|
||||
count++;
|
||||
}
|
||||
|
||||
@@ -350,14 +350,14 @@ function JobRenderService ($q, $compile, $sce, $window, strings) {
|
||||
return list;
|
||||
};
|
||||
|
||||
this.buildRowHTML = (record, ln, content, event) => {
|
||||
this.buildRowHTML = (record, ln, content) => {
|
||||
let id = '';
|
||||
let icon = '';
|
||||
let timestamp = '';
|
||||
let tdToggle = '';
|
||||
let tdEvent = '';
|
||||
let classList = '';
|
||||
let directives = `aw-tool-tip="${this.createToolTip(event, record)}" aw-tip-placement="top"`;
|
||||
let directives = '';
|
||||
|
||||
if (record.isMissing) {
|
||||
return `<div id="${record.uuid}" class="at-Stdout-row">
|
||||
@@ -413,9 +413,9 @@ function JobRenderService ($q, $compile, $sce, $window, strings) {
|
||||
|
||||
if (record && record.isClickable) {
|
||||
classList += ' at-Stdout-row--clickable';
|
||||
directives += ` ng-click="vm.showHostDetails('${record.id}', '${record.uuid}')"
|
||||
`;
|
||||
directives = `ng-click="vm.showHostDetails('${record.id}', '${record.uuid}')"`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div id="${id}" class="at-Stdout-row ${classList}" ${directives}>
|
||||
${tdToggle}
|
||||
@@ -426,16 +426,6 @@ function JobRenderService ($q, $compile, $sce, $window, strings) {
|
||||
`;
|
||||
};
|
||||
|
||||
this.createToolTip = (event, record) => {
|
||||
const status = strings.get('tooltips.HOST_STATUS');
|
||||
const eventID = strings.get('tooltips.EVENT_ID');
|
||||
const clickForDetails = strings.get('tooltips.DETAILS');
|
||||
|
||||
return record.isClickable
|
||||
? `${status} ${event.event_display}<br />${eventID} ${event.id}<br />${clickForDetails}`
|
||||
: `${status} ${event.event_display}<br />${eventID} ${event.id}`;
|
||||
};
|
||||
|
||||
this.getTimestamp = created => {
|
||||
const date = new Date(created);
|
||||
const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours();
|
||||
@@ -629,6 +619,6 @@ function JobRenderService ($q, $compile, $sce, $window, strings) {
|
||||
this.getCapacity = () => OUTPUT_EVENT_LIMIT - (this.getTailCounter() - this.getHeadCounter());
|
||||
}
|
||||
|
||||
JobRenderService.$inject = ['$q', '$compile', '$sce', '$window', 'OutputStrings'];
|
||||
JobRenderService.$inject = ['$q', '$compile', '$sce', '$window'];
|
||||
|
||||
export default JobRenderService;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html ng-csp="no-unsafe-eval">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
@@ -55,7 +55,7 @@ function assignInputGroupValues (apiConfig, credentialType, sourceCredentials) {
|
||||
return input;
|
||||
});
|
||||
|
||||
if (credentialType.get('name') === 'Machine') {
|
||||
if (credentialType.get('namespace') === 'ssh') {
|
||||
const become = inputs.find((field) => field.id === 'become_method');
|
||||
become._isDynamic = true;
|
||||
become._choices = Array.from(apiConfig.become_methods, method => method[0]);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user