Compare commits

...

617 Commits

Author SHA1 Message Date
Sarah Akus
9df447fe75 Merge pull request #12778 from keithjgrant/12542-schedule-exceptions
Schedule exceptions
2022-09-15 16:28:56 -04:00
Keith J. Grant
7e7991bb63 adjust DetailList spacing when two appear in succession 2022-09-15 09:37:03 -07:00
Keith J. Grant
35e9d00beb improve frequency validation performance 2022-09-14 15:33:00 -07:00
Elijah DeLee
461b5221f3 Add graphs for job event processing to dashboard 2022-09-14 16:23:53 -04:00
Elijah DeLee
10d06f219d add alerting rule to grafana
This rule alerts if the redis queue is larger than what the rolling
average event insertion rate/second * 120. In other words, if the redis
queue is larger than it appears we can process events in two minutes.

It appears it has to meet this condition for 60 seconds to start firing.

Future commits will address how to configure contact points like slack.

shout out to @jainnikhil30 and @rebeccahhh who figured this out in jam
session this morning.
2022-09-14 16:23:53 -04:00
Jessica Steurer
a227fea5ef Merge pull request #12868 from keithjgrant/12853-ws-event-duplication
Don't add ws events twice to job output
2022-09-14 16:02:07 -03:00
Jessica Steurer
3f4d0bc15d Merge pull request #12788 from AlexSCorey/5941-Translations
Ensures that strings in helpText files do not miss being translated
2022-09-14 12:02:51 -03:00
Rick Elrod
0812425671 [ui] Minor tweak to capitalize GPG properly (#12734)
"GPG Public Key", not "Gpg Public Key"

Signed-off-by: Rick Elrod <rick@elrod.me>
2022-09-14 01:37:09 +00:00
Alex Corey
94344c0214 Merge pull request #12859 from AlexSCorey/updateCanIUse-lite
updates CanIUseLite
2022-09-13 13:48:20 -04:00
Keith J. Grant
16da9b784a add schedule integration test locators 2022-09-12 16:30:46 -07:00
Keith J. Grant
1e952bab95 fix error message on new schedules with no instances 2022-09-12 12:58:25 -07:00
Jake Jackson
484db004db Update Kind Docs (#12865)
* update kind docs formatting and update some commands

* add tested on fedora update
2022-09-12 13:04:04 -04:00
Alex Corey
7465d7685f updates CanIUseLite 2022-09-09 11:17:54 -04:00
Seth Foster
f0c125efb3 Merge pull request #12762 from akira6592/fix-doc-link
fix link of Patternfly style guide
2022-09-09 09:52:00 -04:00
Keith J. Grant
2d39b81e12 don't add ws events twice to job output 2022-09-08 16:09:02 -07:00
Akira Yokochi
1044d34d98 fix link on doc 2022-09-08 22:49:11 +00:00
Rick Elrod
63567fcc52 [sig validation] better error for job template run (#12735)
When launching a job template, if the last project update failed due to
signature validation, show an error that actually says that.

Signed-off-by: Rick Elrod <rick@elrod.me>
2022-09-08 02:13:41 -05:00
Matthew Jones
cea8c16064 Merge pull request #12724 from mtward/issue-11605
Fix: preserve_existing_hosts flag in awx.awx.group module, while adding a new host to inventory group, retains only 25 existing hosts related #11605
2022-09-07 20:23:58 -04:00
John Westcott IV
e7c97923a3 Merge pull request #12785 from jangel97/devel
Fix list_instances command
* Change from modified to last seen
2022-09-07 14:48:38 -04:00
Keith J. Grant
078c3ae6d8 add schedule form validation to ensure at least one occurrence 2022-09-07 10:33:16 -07:00
Rick Elrod
1ab3dba476 Add "cryptography" kind to CredentialType (#12842)
This was missed when we landed #12813. Adds cryptography
kind to the CredentialType allowed kinds list, which now
produces the proper error message when attempting to PUT
to modify the managed credential type.

Signed-off-by: Rick Elrod <rick@elrod.me>
2022-09-07 12:22:47 -05:00
Alan Rominger
15964dc395 Merge pull request #11745 from AlanCoding/cancel_rework_no_close
Close database connections while processing job output
2022-09-06 15:45:29 -04:00
Keith Grant
b83b65da16 clear output follow mode flag on search (#12791) 2022-09-06 15:15:06 -04:00
Alan Rominger
430f1986c7 Merge pull request #12830 from AlanCoding/dev_stuff
Fix LDAP volume conditional, better metrics interval
2022-09-06 11:51:51 -04:00
Alex Corey
c589f8776c Fixes possible missed translation 2022-09-06 11:26:41 -04:00
Jose Angel Morena
82679ce9a3 replace modified by last_seen in heartbeat 2022-09-06 17:14:19 +02:00
Lila Yasin
6d2e28bfb0 [collection] Add GPG key information to inputs and credential types in documentation. (#12817) 2022-09-06 10:05:36 -05:00
Luiz Costa
7a4da5a8fa Add GPG credential support to awxkit 2022-09-06 10:05:36 -05:00
Rick Elrod
c475a7b6c0 [ui] make signature cred. field be project-global (#12695)
Rather than only allowing the signature credential to be specified on
project using git, allow it to be specified on any project at all.

This moves the field to always show, and moves it out of the git
subform.

Signed-off-by: Rick Elrod <rick@elrod.me>
2022-09-06 10:05:36 -05:00
Rick Elrod
32bb603554 Update action plugin to use ansible-sign library
Signed-off-by: Rick Elrod <rick@elrod.me>
2022-09-06 10:05:36 -05:00
Rick Elrod
8d71292d1a Integrity checking on project sync
Signed-off-by: Rick Elrod <rick@elrod.me>
2022-09-06 10:05:36 -05:00
Veda Periwal
e896dc1aa7 Add Content Signature Validation Credential field to Projects Form page and Projects Detail page 2022-09-06 10:05:36 -05:00
Hao Liu
f5a2246817 add new managed credential type for gpg pub key
add new managed credential type for gpg pub key
add migration file to setup managed credential types to add the new credential type

Signed-off-by: Hao Liu <haoli@redhat.com>
2022-09-06 10:05:36 -05:00
Hao Liu
c467b6ea13 add signature_validation_credential to Project
add new column to `main_project` table
- `signature_validation_credential`

update project module for awx_collection
- added input arg for `signature_validation_credential`

Co-Authored-By: Lila Yasin  <89486372+djyasin@users.noreply.github.com>
2022-09-06 10:05:36 -05:00
Alex Corey
1636f6b196 Merge pull request #12835 from ansible/dependabot/npm_and_yarn/awx/ui/devel/patternfly/patternfly-4.210.2
Bump @patternfly/patternfly from 4.202.1 to 4.210.2 in /awx/ui
2022-09-06 10:33:00 -04:00
Alex Corey
5da528ffbb Merge pull request #12834 from ansible/dependabot/npm_and_yarn/awx/ui/devel/ace-builds-1.10.1
Bump ace-builds from 1.8.1 to 1.10.1 in /awx/ui
2022-09-06 10:30:46 -04:00
Alex Corey
2e65ae49a5 Merge pull request #12806 from ansible/dependabot/npm_and_yarn/awx/ui/devel/luxon-3.0.3
Bump luxon from 3.0.1 to 3.0.3 in /awx/ui
2022-09-06 10:15:08 -04:00
Alex Corey
d06bc815f8 Merge pull request #12807 from ansible/dependabot/npm_and_yarn/awx/ui/devel/dompurify-2.4.0
Bump dompurify from 2.3.10 to 2.4.0 in /awx/ui
2022-09-06 10:14:28 -04:00
dependabot[bot]
0290784f9b Bump @patternfly/patternfly from 4.202.1 to 4.210.2 in /awx/ui
Bumps [@patternfly/patternfly](https://github.com/patternfly/patternfly) from 4.202.1 to 4.210.2.
- [Release notes](https://github.com/patternfly/patternfly/releases)
- [Changelog](https://github.com/patternfly/patternfly/blob/main/RELEASE-NOTES.md)
- [Commits](https://github.com/patternfly/patternfly/compare/prerelease-v4.202.1...prerelease-v4.210.2)

---
updated-dependencies:
- dependency-name: "@patternfly/patternfly"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-06 14:13:52 +00:00
dependabot[bot]
1cc52afc42 Bump ace-builds from 1.8.1 to 1.10.1 in /awx/ui
Bumps [ace-builds](https://github.com/ajaxorg/ace-builds) from 1.8.1 to 1.10.1.
- [Release notes](https://github.com/ajaxorg/ace-builds/releases)
- [Changelog](https://github.com/ajaxorg/ace-builds/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ajaxorg/ace-builds/compare/v1.8.1...v1.10.1)

---
updated-dependencies:
- dependency-name: ace-builds
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-06 14:13:17 +00:00
Alex Corey
88f7f987cd Merge pull request #12810 from ansible/dependabot/npm_and_yarn/awx/ui/devel/patternfly/react-table-4.100.8
Bump @patternfly/react-table from 4.93.1 to 4.100.8 in /awx/ui
2022-09-06 10:12:01 -04:00
Alan Rominger
f512971991 Add project sync to job cancel chain 2022-09-05 22:29:19 -04:00
Alan Rominger
53de245877 Fix LDAP volume conditional, better metrics interval 2022-09-04 22:33:12 -04:00
Shane McDonald
749622427c Merge pull request #12825 from shanemcd/extend-includes
Extend black excludes instead of overriding
2022-09-02 15:40:41 -04:00
Alan Rominger
725d6fa896 Merge pull request #12820 from AlanCoding/five_seconds
Make the metrics default sampling interval 5s
2022-09-02 15:21:57 -04:00
Shane McDonald
a107bb684c Extend black excludes instead of overriding
By default it will ignore things in .gitignore, which we want
2022-09-02 15:11:45 -04:00
Alan Rominger
ccbc8ce7de Make the metrics default sampling interval 5s 2022-09-02 13:38:49 -04:00
Shane McDonald
260e1d4f2d Make static asset location consistent across all deployments (#12819) 2022-09-02 17:12:06 +00:00
Shane McDonald
1afa49f3ff Merge pull request #12632 from TheRealHaoLiu/kind-k8s-devel
Add documentation for running development environment in kind
2022-09-02 12:12:01 -04:00
Rick Elrod
6f88ea1dc7 Common Inventory slicing method for job slices
- Extract how slicing is done from Inventory#get_script_data and pull it
  into a new method, Inventory#get_sliced_hosts
- Make use of this method in Inventory#get_script_data
- Make use of this method in Job#_get_inventory_hosts (used by
  Job#start_job_fact_cache and Job#finish_job_fact_cache).

This fixes an issue (namely in Tower 4.1) where job slicing with fact
caching enabled doesn't save facts for all hosts.

Signed-off-by: Rick Elrod <rick@elrod.me>
2022-09-01 16:15:07 -05:00
Alan Rominger
c59bbdecdb Refactor canceling to work through messaging and signals, not database
If canceled attempted before, still allow attempting another cancel
in this case, attempt to send the sigterm signal again.
Keep clicking, you might help!

Replace other cancel_callbacks with sigterm watcher
  adapt special inventory mechanism for this too

Get rid of the cancel_watcher method with exception in main thread

Handle academic case of sigterm race condition

Process cancelation as control signal

Fully connect cancel method and run_dispatcher to control

Never transition workflows directly to canceled, add logs
2022-09-01 15:20:31 -04:00
Matthew Jones
f9428c10b9 Merge pull request #12803 from matburt/fix_cleanup_schedules
Fix an issue where default cleanup schedules only run once
2022-09-01 10:40:11 -04:00
dependabot[bot]
1ca054f43d Bump @patternfly/react-table from 4.93.1 to 4.100.8 in /awx/ui
Bumps [@patternfly/react-table](https://github.com/patternfly/patternfly-react) from 4.93.1 to 4.100.8.
- [Release notes](https://github.com/patternfly/patternfly-react/releases)
- [Commits](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-table@4.93.1...@patternfly/react-table@4.100.8)

---
updated-dependencies:
- dependency-name: "@patternfly/react-table"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-01 08:09:44 +00:00
dependabot[bot]
374f76b527 Bump dompurify from 2.3.10 to 2.4.0 in /awx/ui
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.3.10 to 2.4.0.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.3.10...2.4.0)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-01 08:08:10 +00:00
dependabot[bot]
f9dd5e0f1c Bump luxon from 3.0.1 to 3.0.3 in /awx/ui
Bumps [luxon](https://github.com/moment/luxon) from 3.0.1 to 3.0.3.
- [Release notes](https://github.com/moment/luxon/releases)
- [Changelog](https://github.com/moment/luxon/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moment/luxon/compare/3.0.1...3.0.3)

---
updated-dependencies:
- dependency-name: luxon
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-01 08:07:33 +00:00
Matthew Jones
bb7509498e Fix an issue where default cleanup schedules only run once
This looks like an oversight that has existed for a long time. We intend to run these on a pretty regular basis
2022-08-31 20:10:20 -04:00
Keith Grant
8a06ffbe15 poll for events processing completion (#12689) 2022-08-31 16:03:35 -04:00
Hao Liu
8ad948f268 Merge pull request #12797 from TheRealHaoLiu/remove-helm-from-dockerfile
remove helm from dockerfile template
2022-08-31 14:18:25 -04:00
Hao Liu
73f808dee7 remove helm from dockerfile template
Signed-off-by: Hao Liu <haoli@redhat.com>
2022-08-31 13:48:30 -04:00
Shane McDonald
fecab52f86 Merge pull request #12796 from shanemcd/fix-tests
Prevent openldap from getting downgraded during build
2022-08-31 13:34:04 -04:00
Shane McDonald
609c67d85e Prevent openldap from getting downgraded during build
We noticed here that openldap was getting downgraded and caused our test suite to blow up https://github.com/ansible/awx/runs/8118323342?check_suite_focus=true
2022-08-31 13:09:29 -04:00
Keith J. Grant
0005d249c0 update tests 2022-08-30 15:44:52 -07:00
Hao Liu
8828ea706e add make target for building custom awx kube image (#12789) 2022-08-30 20:19:36 +00:00
Shane McDonald
4070ef3f33 Merge pull request #12787 from shanemcd/pre-build-ui
Speed up image build when UI is pre-built on host
2022-08-30 15:51:43 -04:00
Keith Grant
39f6e2fa32 fix TypeError when config is undefined (#12697) 2022-08-30 15:11:45 -04:00
Shane McDonald
1dfdff4a9e Speed up image build when UI is pre-built on host 2022-08-30 12:36:25 -04:00
Alan Rominger
310e354164 Merge pull request #12769 from AlanCoding/self_conn
Fix sanity check to use the relevant active connection
2022-08-29 20:36:48 -04:00
Keith J. Grant
dda2931e60 fix exception frequency placeholder text 2022-08-29 13:43:49 -07:00
Alan Rominger
6d207d2490 Merge pull request #12754 from kdelee/fix_metrics_consumed_capacity
calcuate consumed capacity in same way in metrics
2022-08-29 16:37:53 -04:00
Alan Rominger
01037fa561 Fix sanity check to use the relevant active connection 2022-08-29 16:33:07 -04:00
Alan Rominger
61f3e5cbed Merge pull request #12702 from AlanCoding/poll_cancel
Check exit conditions in loop waiting for project flock
2022-08-29 16:29:39 -04:00
Alan Rominger
44995e944a Merge pull request #12766 from AlanCoding/lazy_no_more
Revert "Merge pull request #12584 from AlanCoding/lazy_workers"
2022-08-29 16:06:50 -04:00
Keith J. Grant
4a92fcfc62 add schedule exceptions to details 2022-08-29 11:55:32 -07:00
Elijah DeLee
d3f15f5784 Merge pull request #4 from AlanCoding/elijah_metrics
Minor changes to instance loop structure
2022-08-29 14:33:46 -04:00
Alan Rominger
2437a84b48 Minor changes to instance loop structure 2022-08-29 14:28:50 -04:00
Shane McDonald
696f099940 Merge pull request #12749 from shanemcd/not-so-aggressive
Make error handling less aggressive when checking status of dispatcher task
2022-08-29 11:50:56 -04:00
Shane McDonald
3f0f538c40 Merge pull request #12759 from shanemcd/auto-prom
Automate bootstrapping of Prometheus in the development environment
2022-08-29 11:25:13 -04:00
Shane McDonald
66529d0f70 Automate bootstrapping of Prometheus in the development environment 2022-08-29 09:39:44 -04:00
Alan Rominger
974f845059 Revert "Merge pull request #12584 from AlanCoding/lazy_workers"
This reverts commit 64157f7207, reversing
changes made to 9e8ba6ca09.
2022-08-28 23:04:13 -04:00
Keith J. Grant
f6b3413a11 add schedule exemptions to form 2022-08-26 16:00:08 -07:00
Shane McDonald
b4ef687b60 Merge pull request #12760 from shanemcd/another-domino-falls
Fix browsable API in development environment
2022-08-26 17:43:37 -04:00
Shane McDonald
2ef531b2dc Fix browsable API in development environment
Fallout from https://github.com/ansible/awx/pull/12722
2022-08-26 17:19:16 -04:00
Elijah DeLee
125801ec5b add panel to grafana dashboard for capacity
also reorganize so there are two columns of panels, not
just one long skinny set of panels
2022-08-26 15:42:40 -04:00
Shane McDonald
691d9d7dc4 Merge pull request #12755 from shanemcd/fix-dev-env-admin-pw
Fix auto-generated dev env admin password
2022-08-26 13:33:43 -04:00
Shane McDonald
5ca898541f Fix auto-generated dev env admin password
Fallout from https://github.com/ansible/awx/pull/12753
2022-08-26 13:07:46 -04:00
Shane McDonald
24821ff030 Merge pull request #12753 from shanemcd/custom-dev-env-admin-pw
Allow for setting custom admin password in dev environment
2022-08-26 11:55:17 -04:00
Elijah DeLee
99815f8962 calcuate consumed capacity in same way in metrics
We should be consistent about this. Also this takes us from doing a as
many queries to the UnifiedJob table as we have instances to doing 1
query to the UnifiedJob table (and both do 1 query to Instances table)
2022-08-26 11:40:36 -04:00
Shane McDonald
d752e6ce6d Allow for setting custom admin password in dev environment 2022-08-26 11:29:11 -04:00
Shane McDonald
457dd890cb Make error handling less aggressive when checking status of dispatcher task 2022-08-26 09:05:38 -04:00
Christian Adams
4fbf5e9e2f Merge pull request #12731 from rooftopcellist/fix-messages-target
Fix make target for compiling api strings
2022-08-24 17:01:43 -04:00
Christian M. Adams
687b4ac71d Fix make target for compiling api strings 2022-08-24 16:36:25 -04:00
John Westcott IV
a1b364f80c Configuring Keycloak to also do OIDC (#12700) 2022-08-24 07:08:39 -04:00
mtward
271938c5fc Update group.py 2022-08-23 15:06:11 -04:00
Jessica Steurer
ff49cc5636 Merge pull request #12552 from whitej6/jlw-generic-oidc
Implement Generic OIDC Provider
2022-08-23 15:38:43 -03:00
Shane McDonald
9946e644c8 Merge pull request #12722 from shanemcd/fix-static-root
Fix STATIC_ROOT in defaults
2022-08-23 12:58:12 -04:00
Shane McDonald
1ed7a50755 Fix STATIC_ROOT in defaults
Reasoning:

- This is breaking the UI in official image builds of devel
- This is always being overridden in our packaging
- PROJECTS_ROOT and JOBOUTPUT_ROOT also hardcode /var/lib/awx
2022-08-23 12:39:54 -04:00
Jeremy White
9f3396d867 rebasing 2022-08-23 09:51:04 -05:00
John Westcott IV
bcd018707a Adding ability to auto-apply community label to PRs and Issues (#12718) 2022-08-23 07:08:24 -04:00
Shane McDonald
a462978433 Merge pull request #12699 from shanemcd/remove-settings-py-during-build
Remove need for settings.py during image build
2022-08-22 14:13:36 -04:00
Shane McDonald
6d11003975 Remove need for settings.py during image build 2022-08-22 13:46:42 -04:00
Shane McDonald
017e474325 Merge pull request #12704 from shanemcd/dynamic-log-config
Consolidate and refactor logging configuration code
2022-08-22 13:31:28 -04:00
Alex Corey
5d717af778 Merge pull request #12713 from AlexSCorey/CustomizeDependatPRBodies
Edits existing PR body
2022-08-22 12:24:25 -04:00
Alex Corey
8d08ac559d Puts new pr string on a new line 2022-08-22 12:05:43 -04:00
Shane McDonald
4e24867a0b Merge pull request #12703 from shanemcd/ded-code
Delete unused playbook profiling code
2022-08-22 11:33:37 -04:00
Alex Corey
2b4b8839d1 Edits existing PR body 2022-08-22 11:31:49 -04:00
Yuki Yamashita
dba33f9ef5 Replace gethostbyname to getaddrinfo for plugins ipv6 support related #11450 (#12561)
Co-authored-by: yukiy <yyamashi@redhat.com>
2022-08-22 11:07:10 -03:00
Julen Landa Alustiza
db2649d7ba Merge pull request #12706 from ansible/revert-12692-mop_up
Revert "Fix errors in websocket code due to missing template"
2022-08-22 15:53:35 +02:00
Alan Rominger
edc3da85cc Revert "Fix errors in websocket code due to missing template" 2022-08-20 19:09:57 -04:00
Alan Rominger
2357e24d1d Merge pull request #12701 from AlanCoding/no_more_schedules
Make schedule teardown more reliable
2022-08-20 07:05:21 -04:00
Shane McDonald
e4d1056450 Change log level for UnifiedJob#log_lifecycle 2022-08-19 17:56:17 -04:00
Shane McDonald
37d9c9eb1b Consolidate and refactor logging configuration code 2022-08-19 17:16:27 -04:00
Shane McDonald
d42a85714a Delete unused playbook profiling code
We haven't had this feature since pre-AWX 18 (since EEs were introduced) and I cant find any other reference to this.
2022-08-19 17:03:22 -04:00
Alan Rominger
88bf03c6bf Check exit conditions in loop waiting for project flock 2022-08-19 16:08:56 -04:00
Alan Rominger
4b8a56be39 Make schedule teardown more reliable 2022-08-19 15:42:00 -04:00
Alan Rominger
2aa99234f4 Merge pull request #12692 from AlanCoding/mop_up
Fix errors in websocket code due to missing template
2022-08-19 14:46:10 -04:00
Michael Abashian
bf9f1b1d56 Added more context to subscription details and rearrange the order of some of the fields (#12649)
* Adds more context to subscription details and rearranges some of the fields

* Fixes broken unit test after updating subscription details
2022-08-19 09:41:23 -04:00
Alan Rominger
704e4781d9 Fix errors in websocket code due to missing template 2022-08-18 14:05:06 -04:00
Alan Rominger
4a8613ce4c Avoid updating modified_by from None to None (#11838)
This should help the case of inventory updates in particular
  where imported hosts are managed by the system
2022-08-18 11:39:29 -04:00
Alan Rominger
e87fabe6bb Submit job to dispatcher as part of transaction (#12573)
Make it so that submitting a task to the dispatcher happens as part of the transaction.
  this applies to dispatcher task "publishers" which NOTIFY the pg_notify queue
  if the transaction is not successful, it will not be sent, as per postgres docs

This keeps current behavior for pg_notify listeners
  practically, this only applies for the awx-manage run_dispatcher service
  this requires creating a separate connection and keeping it long-lived
  arbitrary code will occasionally close the main connection, which would stop listening

Stop sending the waiting status websocket message
  this is required because the ordering cannot be maintained with other changes here
  the instance group data is moved to the running websocket message payload

Move call to create_partition from task manager to pre_run_hook
  mock this in relevant unit tests
2022-08-18 09:43:53 -04:00
Alan Rominger
532aa83555 Merge pull request #11833 from AlanCoding/facts_update_fields
Use update_fields for Ansible facts update
2022-08-17 22:37:45 -04:00
Alan Rominger
d87bb973d5 Merge pull request #12090 from AlanCoding/mind_your_own_business
Avoid parent instance update when status was unchanged
2022-08-17 22:29:31 -04:00
Alan Rominger
a72da3bd1a Merge pull request #12582 from AlanCoding/clean_and_forget
Move reaper logic into worker, avoiding bottlenecks
2022-08-17 18:53:47 -04:00
Alan Rominger
56df3f0c2a Merge pull request #12671 from AlanCoding/cut_the_line
Avoid dependency manager for jobs with no deps
2022-08-17 18:50:52 -04:00
Alan Rominger
e0c59d12c1 Change data structure so we can conditionally reap waiting jobs 2022-08-17 16:00:30 -04:00
Alan Rominger
7645cc2707 Remove mocks for reap method that was removed 2022-08-17 15:43:29 -04:00
Alan Rominger
6719010050 Add back in cleanup call 2022-08-17 15:42:48 -04:00
Alan Rominger
ccd46a1c0f Move reaper logic into worker, avoiding bottlenecks 2022-08-17 15:42:47 -04:00
Alex Corey
cc1e349ea8 Merge pull request #12604 from ansible/dependabot/npm_and_yarn/awx/ui/devel/ace-builds-1.8.1
Bump ace-builds from 1.6.0 to 1.8.1 in /awx/ui
2022-08-17 14:11:27 -04:00
Alex Corey
e509d5f1de Merge pull request #12606 from ansible/dependabot/npm_and_yarn/awx/ui/devel/dompurify-2.3.10
Bump dompurify from 2.3.8 to 2.3.10 in /awx/ui
2022-08-17 14:10:51 -04:00
Alan Rominger
4fca27c664 Merge pull request #12289 from AlanCoding/idle_help
Correct help text for job idle timeout
2022-08-17 13:55:44 -04:00
Alan Rominger
51be22aebd Merge pull request #12668 from AlanCoding/graph_tweaks
Remove an old metrics field and add a new one to dashboard
2022-08-17 13:49:17 -04:00
Alan Rominger
54b21e5872 Avoid dependency manager for jobs with no deps 2022-08-17 13:32:59 -04:00
Alan Rominger
85beb9eb70 Merge pull request #12676 from AlanCoding/forward_picks
Stability fixes, and related logging for slowdowns in dispatcher task processing
2022-08-17 13:32:34 -04:00
Alan Rominger
56739ac246 Use delay_update to set error message, according to merge note 2022-08-17 11:45:40 -04:00
Alan Rominger
1ea3c564df Apply a failed status if cancel_flag is not set 2022-08-17 11:42:09 -04:00
Alan Rominger
621833ef0e Add extra workers if computing based on memory
Co-authored-by: Elijah DeLee <kdelee@redhat.com>
2022-08-17 11:41:59 -04:00
Shane McDonald
16be38bb54 Allow for passing custom job_explanation to reaper methods
Co-authored-by: Alan Rominger <arominge@redhat.com>
2022-08-17 11:41:49 -04:00
Shane McDonald
c5976e2584 Add setting for missed heartbeats before marking node offline 2022-08-17 11:39:30 -04:00
Shane McDonald
3c51cb130f Add grace period settings for task manager timeout, and pod / job waiting reapers
Co-authored-by: Alan Rominger <arominge@redhat.com>
2022-08-17 11:39:01 -04:00
Shane McDonald
c649809eb2 Remove debug method that calls cleanup
- It's unclear why this was here.
- Removing it doesnt appear to cause any problems.
- It still gets called during heartbeats.
2022-08-17 11:35:43 -04:00
Alan Rominger
43a53f41dd Add logs about heartbeat skew
Co-authored-by: Shane McDonald <me@shanemcd.com>
2022-08-17 11:33:59 -04:00
Alan Rominger
a3fef27002 Add logs to debug waiting bottlenecking 2022-08-17 11:33:49 -04:00
Alan Rominger
cfc1255812 Merge pull request #12442 from AlanCoding/waiting_reaper
Fix false reaper false-positives of waiting jobs that are waiting for worker
2022-08-17 11:20:05 -04:00
Alan Rominger
278db2cdde Split reaper for running and waiting jobs
Avoid running jobs that have already been reapted

Co-authored-by: Elijah DeLee <kdelee@redhat.com>

Remove unnecessary extra actions

Fix waiting jobs in other cases of reaping
2022-08-17 10:53:29 -04:00
Alan Rominger
64157f7207 Merge pull request #12584 from AlanCoding/lazy_workers
Wait 60 seconds before scaling down a worker
2022-08-17 10:18:19 -04:00
Alan Rominger
9e8ba6ca09 Merge pull request #12494 from AlanCoding/revival
Register system again if deleted by another pod
2022-08-17 10:12:39 -04:00
Alan Rominger
268ab128d7 Merge pull request #12527 from AlanCoding/offline_db
Further resiliency changes, specifically focused on case of database going offline
2022-08-17 10:10:50 -04:00
Alan Rominger
fad5934c1e Merge pull request #12356 from AlanCoding/copytree_neo
Replace git shallow clone with shutil.copytree
2022-08-17 10:07:28 -04:00
Alan Rominger
c9e3873a28 Use update_fields for Ansible facts update 2022-08-17 08:22:41 -04:00
Jessica Steurer
6a19aabd44 feature_request_form_update (#12625)
* Feature_update

* Feature_update

* update-feature-request

* update-edit
2022-08-17 08:52:30 -03:00
Alan Rominger
11e63e2e89 Remove an old metrics field and add a new one to dashboard 2022-08-16 22:37:27 -04:00
Hao Liu
7c885dcadb add help command to make (#12669)
add `make help`
that prints all available make targets
help text generated from comments above the make target starting with `##`

Signed-off-by: Hao Liu <haoli@redhat.com>
2022-08-16 20:36:47 -04:00
John Westcott IV
b84a192bad Altering events relationship to hosts to increase performance (#12447)
Removing cascade on delete at model level that could cause locking issues.
2022-08-16 12:03:05 -04:00
Elijah DeLee
35afb10add fix use of distinct on query that UI
When on the screen in the UI that loads the job events, the ui includes
a filter to exclude job events where stdout = ''. Because this is a
TextField and was not in the allow list, we were applying DISTINCT to
the query. This made it very unperformant for large jobs, especially
on the query that gets the count and cannot put a LIMIT on the query.

Also correctly prefetch the related job_template data on the view to
cut down the number of queries we make from around 50 to under 10.

We need to analyze other similar views for other prefetch type
optimizations we should make.
2022-08-16 10:08:33 -04:00
Hao Liu
13fc845bcc develop AWX on MacOS using K8S
Add instruction for AWX development on MacOS using Kind Cluster

Signed-off-by: Hao Liu <haoli@redhat.com>
2022-08-15 22:48:23 -04:00
Alan Rominger
f1bd1f1dfc Merge pull request #12658 from AlanCoding/more_panels
Add more graphs for task manager refactor
2022-08-15 16:07:43 -04:00
Sarah Akus
67c9e1a0cb Merge pull request #12650 from matburt/fix_default_adhoc_verbosity
Fixed a bug where the initial form value of verbosity isn't respected
2022-08-15 15:48:49 -04:00
Alan Rominger
f6da9a5073 Add more graphs for task manager refactor 2022-08-15 15:29:34 -04:00
Seth Foster
38a0950f46 Merge pull request #12656 from fosterseth/metrics_tm_on_commit
Add metric for task manager on_commit calls
2022-08-15 13:54:34 -04:00
Seth Foster
55d295c2a6 Add metric to measure task manager transaction, including on_commit calls 2022-08-15 12:44:29 -04:00
Elijah DeLee
be45919ee4 have postgres log to console in dev env
also log slow queries and link to documentation for other possible
settings
2022-08-15 12:09:17 -04:00
mabashian
0a4a9f96c2 Explicitly set value for verbosity to 0 as the default value which corresponds to 0 (Normal) 2022-08-12 14:03:36 -04:00
Matthew Jones
1ae1da3f9c Fix a bug where the form value of verbosity isn't respect 2022-08-12 09:29:31 -04:00
Keith Grant
cae2c06190 Complex schedules UI (#12445)
* refactor ScheduleFormFields into own file

* refactor ScheduleForm

* wip complex schedules form

* build rruleset from inputs

* update schedule form validation for multiple repeat frequencies

* add basic rrule set parsing when opening schedule form

* complex schedule bugfixes, handle edge cases, etc

* fix schedule saving/parsing for single-occurrence schedules

* working with timezone issues

* fix rrule until times to be in UTC

* update tests for new schedule form format

* update ouiaIds

* tweak schedules spacing

* update ScheduleForm tests

* show message for unsupported schedule types

* default schedules to browser timezone

* show error type/message in ErrorDetail

* shows frequencies on ScheduleDetails view

* handles nullish values
2022-08-11 16:55:52 -04:00
John Westcott IV
993dd61024 Forcing an unbind for a django-auth-ldap sticky session to the LDAP server (#12367)
* Forcing an unbind for a django-auth-ldap sticky session to the LDAP server

* Focring _connection_bound to false after closing and modifying exceptino logging
2022-08-11 16:46:41 -03:00
Alan Rominger
ea07aef73e Correct help text for job idle timeout 2022-08-11 09:39:29 -04:00
John Westcott IV
268a4ad32d Modifying reaper of administrative work units to allow for change from Controller to Hybrid nodes (#12614) 2022-08-11 09:03:35 -03:00
Sean Sullivan
3712af4df8 update role to provide better error messages (#12599) 2022-08-11 07:09:11 -04:00
Sean Sullivan
8cf75fce8c Update awx collection workflow nodes to look for type (#12597) 2022-08-11 07:08:27 -04:00
Alan Rominger
46be2d9e5b Replace git shallow clone with shutil.copytree
Introduce build_project_dir method
  the base method will create an empty project dir for workdir

Share code between job and inventory tasks with new mixin
  combine rest of pre_run_hook logic
  structure to hold lock for entire sync process

force sync to run for inventory updates due to UI issues

Remove reference to removed scm_last_revision field
2022-08-10 16:18:56 -04:00
Alan Rominger
998000bfbe Surface correct error from bulk_create on unrecoverable error 2022-08-10 16:16:57 -04:00
Alan Rominger
43a50cc62c Fix event counting in error handling path 2022-08-10 16:16:57 -04:00
Alan Rominger
30f556f845 Further resiliency changes focused on offline database
Make logs from database outage more manageable

Raise exception if update_model never recovers from problem
2022-08-10 16:16:57 -04:00
Alan Rominger
c5985c4c81 Change lazy worker method name and adjust log 2022-08-10 16:12:03 -04:00
Alan Rominger
a9170236e1 Wait 60 seconds before scaling down a worker 2022-08-10 16:12:03 -04:00
Seth Foster
85a5b58d18 Merge pull request #12629 from fosterseth/task_manager_refactor_squashed
Task manager refactor
2022-08-10 16:02:05 -04:00
Seth Foster
6fb3c8daa8 Merge pull request #44 from AlanCoding/one_of_seths_own
Inherit from our own APIView, not rest framework
2022-08-10 15:38:14 -04:00
Alan Rominger
a0103acbef Inherit from our own APIView, not rest framework 2022-08-10 15:31:19 -04:00
Alan Rominger
f7e6a32444 Optimize task manager with debug toolbar, adjust prefetch (#12588) 2022-08-10 10:05:13 -04:00
Alex Corey
7bbc256ff1 Merge pull request #12637 from AlexSCorey/12636-WorkflowApprovalTranslations
Fixes lack of translation on workflow approval list item actions
2022-08-09 15:47:34 -04:00
Alex Corey
64f62d6755 fixes translation issue 2022-08-09 15:30:08 -04:00
Alex Corey
b4cfe868fb Merge pull request #12546 from mabashian/6018-node-alias
Fix bug where node alias is not remaining after changing the template on a wf node
2022-08-09 10:16:46 -04:00
Alex Corey
8d8681580d Merge pull request #12548 from AlexSCorey/12512-UpdateWorkflowApprovalToolbar
Refactors and redesigns workflow approval to impove UX
2022-08-09 10:02:27 -04:00
Alex Corey
8892cf2622 Adds toast to workflow approval on cancel 2022-08-09 09:40:34 -04:00
Alan Rominger
585d3f4e2a Register system again if deleted by another pod
Avoid cases where missing instance
  would throw error on startup
  this gives time for heartbeat to register it
2022-08-08 22:36:17 -04:00
Alex Corey
2c9a0444e6 Easier review workflow output (#12459)
* Adds new tab component and positions it properly on screen

* Adds filtering, and navigation to node outputs
2022-08-08 16:13:51 -04:00
Alan Rominger
279cebcef3 Merge pull request #12586 from AlanCoding/connections_graph
Add a graph to show database connections being used
2022-08-08 15:49:20 -04:00
Seth Foster
e6f8852b05 Cache task_impact
task_impact is now a field on the database
It is calculated and set during create_unified_job

set task_impact on .save for adhoc commands
2022-08-05 14:33:47 -04:00
Alan Rominger
d06a3f060d Block sliced workflow jobs on any job type from their JT (#12551) 2022-08-05 14:33:45 -04:00
Seth Foster
957b2b7188 Cache preferred instance groups
When creating unified job, stash the list of pk values from the
instance groups returned from preferred_instance_groups so that the
task management system does not need to call out to this method
repeatedly.

.preferred_instance_groups_cache is the new field
2022-08-05 14:33:28 -04:00
Alan Rominger
b94b3a1e91 [task_manager_refactor] Move approval node expiration logic into queryset (#12502)
Instead of loading all pending Workflow Approvals in the task manager,
  run a query that will only return the expired apporovals
  directly expire all which are returned by that query

Cache expires time as a new field in order to simplify WorkflowApproval filter
2022-08-05 14:33:27 -04:00
Elijah DeLee
7776a81e22 add job to dependency graph in start task
We always add the job to the graph right before calling start task.
Reduce complexity of proper operation by just doing this in start_task,
because if you call start_task, you need to add it to the dependency
graph
2022-08-05 14:33:26 -04:00
Elijah DeLee
bf89093fac unify call pattern for get_tasks 2022-08-05 14:33:26 -04:00
Elijah DeLee
76d76d13b0 Start pending workflows in TaskManager
we had tried doing this in the WorkflowManager, but we decided that
we want to handle ALL pending jobs and "soft blockers" to jobs with the
TaskManager/DependencyGraph and not duplicate that logic in the
WorkflowManager.
2022-08-05 14:33:26 -04:00
Elijah DeLee
e603c23b40 fix sliced jobs blocking logic in depedency graph
We have to look at the sliced job's unified_job_template_id
Now, task_blocked_by works for sliced jobs too.
2022-08-05 14:33:26 -04:00
Alan Rominger
8af4dd5988 Fix unintended slice job blocking 2022-08-05 14:33:25 -04:00
Seth Foster
0a47d05d26 split schedule_task_manager into 3
each call to schedule_task_manager becomes one of

ScheduleTaskManager
ScheduleDependencyManager
ScheduleWorkflowManager
2022-08-05 14:33:25 -04:00
Seth Foster
b3eb9e0193 pid kill each of the 3 task managers on timeout 2022-08-05 14:33:25 -04:00
Elijah DeLee
b26d2ab0e9 fix looking at wrong id for wf allow_simultaneous 2022-08-05 14:33:25 -04:00
Elijah DeLee
7eb0c7dd28 exit task manager loops early if we are timed out
add settings to define task manager timeout and grace period

This gives us still TASK_MANAGER_TIMEOUT_GRACE_PERIOD amount of time to
get out of the task manager.

Also, apply start task limit in WorkflowManager to starting pending
workflows
2022-08-05 14:33:24 -04:00
Elijah DeLee
236c1df676 fix lint errors 2022-08-05 14:33:24 -04:00
Seth Foster
ff118f2177 Manage pending workflow jobs in Workflow Manager
get_tasks uses UnifiedJob
Additionally, make local overrides run after development settings
2022-08-05 14:31:48 -04:00
Elijah DeLee
29d91da1d2 we can do all the work in one loop
more than saving the loop, we save building the WorkflowDag twice which
makes LOTS of queries!!!

Also, do a bulk update on the WorkflowJobNodes instead of saving in a
loop :fear:
2022-08-05 14:31:48 -04:00
Elijah DeLee
ad08eafb9a add debug views for task manager(s)
implement https://github.com/ansible/awx/issues/12446
in development environment, enable set of views that run
the task manager(s).

Also introduce a setting that disables any calls to schedule()
that do not originate from the debug views when in the development
environment. With guards around both if we are in the development
environment and the setting, I think we're pretty safe this won't get
triggered unintentionally.

use MODE to determine if we are in devel env

Also, move test for skipping task managers to the tasks file
2022-08-05 14:31:24 -04:00
Seth Foster
431b9370df Split TaskManager into
- DependencyManager spawns dependencies if necessary
- WorkflowManager processes running workflows to see if a new job is
  ready to spawn
- TaskManager starts tasks if unblocked and has execution capacity
2022-08-05 14:29:02 -04:00
Alex Corey
3e93eefe62 Merge pull request #12618 from vedaperi/3999-NotificationHelpText
Add Help Text with documentation link to Notification Templates page
2022-08-05 10:41:07 -04:00
John Westcott IV
782667a34e Allow multiple values in SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR.is_*_[value|role] settings (#12558) 2022-08-05 10:39:50 -04:00
dependabot[bot]
90524611ea Bump ace-builds from 1.6.0 to 1.8.1 in /awx/ui
Bumps [ace-builds](https://github.com/ajaxorg/ace-builds) from 1.6.0 to 1.8.1.
- [Release notes](https://github.com/ajaxorg/ace-builds/releases)
- [Changelog](https://github.com/ajaxorg/ace-builds/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ajaxorg/ace-builds/compare/v1.6.0...v1.8.1)

---
updated-dependencies:
- dependency-name: ace-builds
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-05 14:39:49 +00:00
dependabot[bot]
583086ae62 Bump dompurify from 2.3.8 to 2.3.10 in /awx/ui
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.3.8 to 2.3.10.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.3.8...2.3.10)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-05 14:39:23 +00:00
Alex Corey
19c24cba10 Merge pull request #12602 from ansible/dependabot/npm_and_yarn/awx/ui/devel/prop-types-15.8.1
Bump prop-types from 15.7.2 to 15.8.1 in /awx/ui
2022-08-04 09:56:23 -04:00
Jeff Bradberry
5290c692c1 Merge pull request #12620 from jbradberry/even-narrower-reload
Restrict files that trigger a reload
2022-08-04 09:21:31 -04:00
Jeff Bradberry
90a19057d5 Restrict files that trigger a reload
to files explicitly ending in '.py' that do not start with a dot.
This will avoid Emacs lockfiles from triggering the restart.
2022-08-03 18:23:48 -04:00
dependabot[bot]
a05c328081 Bump prop-types from 15.7.2 to 15.8.1 in /awx/ui
Bumps [prop-types](https://github.com/facebook/prop-types) from 15.7.2 to 15.8.1.
- [Release notes](https://github.com/facebook/prop-types/releases)
- [Changelog](https://github.com/facebook/prop-types/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/prop-types/compare/v15.7.2...v15.8.1)

---
updated-dependencies:
- dependency-name: prop-types
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-03 16:09:07 +00:00
Alex Corey
6d9e353a4e Merge pull request #12603 from ansible/dependabot/npm_and_yarn/awx/ui/devel/rrule-2.7.1
Bump rrule from 2.7.0 to 2.7.1 in /awx/ui
2022-08-03 12:06:51 -04:00
Alex Corey
82c062eab9 Merge pull request #12605 from ansible/dependabot/npm_and_yarn/awx/ui/devel/luxon-3.0.1
Bump luxon from 2.4.0 to 3.0.1 in /awx/ui
2022-08-03 12:06:32 -04:00
vedaperi
c0d59801d5 Add help text to Notification Templates form and detail with link to documentation 2022-08-02 18:15:56 -07:00
Alex Corey
93ea8a0919 Adds toast to detail view and fixes non-disabled action button on list view 2022-08-02 17:18:29 -04:00
Rebeccah Hunter
67f1ab2237 Merge pull request #12609 from john-westcott-iv/oracle_awx_triage_reply
Adding triage response for inquaries around Oracles version of AWX
2022-08-01 13:53:02 -04:00
John Westcott IV
71be8fadcb Adding GitHub check to ensure PRs have the proper X/Y/Z flags (#12577)
* Adding GitHub check to ensure PRs have the proper X/Y/Z flags
* Changing the Z release wording
2022-08-01 12:59:01 -04:00
John Westcott IV
c41becec13 Adding triage response for inquaries around Oracles version of AWX 2022-08-01 12:00:48 -04:00
mabashian
6d0d8e57a4 Fix bug where node alias is not remaining after changing the template on a wf node 2022-08-01 11:28:50 -04:00
Shane McDonald
6446b627ad Merge pull request #12608 from shanemcd/fix-k8s-dev-env
Fix Kubernetes dev environment + update docs
2022-08-01 11:11:45 -04:00
Shane McDonald
fcebd188a6 Fix Kubernetes dev environment + update docs 2022-08-01 10:45:10 -04:00
Alex Corey
1fca505b61 Refactors and redesigns workflow approval to impove UX 2022-08-01 09:59:53 -04:00
dependabot[bot]
a0e9c30b4a Bump luxon from 2.4.0 to 3.0.1 in /awx/ui
Bumps [luxon](https://github.com/moment/luxon) from 2.4.0 to 3.0.1.
- [Release notes](https://github.com/moment/luxon/releases)
- [Changelog](https://github.com/moment/luxon/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moment/luxon/compare/2.4.0...3.0.1)

---
updated-dependencies:
- dependency-name: luxon
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 08:11:23 +00:00
dependabot[bot]
bc94dc0257 Bump rrule from 2.7.0 to 2.7.1 in /awx/ui
Bumps [rrule](https://github.com/jakubroztocil/rrule) from 2.7.0 to 2.7.1.
- [Release notes](https://github.com/jakubroztocil/rrule/releases)
- [Changelog](https://github.com/jakubroztocil/rrule/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jakubroztocil/rrule/compare/v2.7.0...v2.7.1)

---
updated-dependencies:
- dependency-name: rrule
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 08:10:38 +00:00
Shane McDonald
65771b7629 Merge pull request #12562 from shanemcd/auto-install-setuptools-scm
Automatically install setuptools-scm in script called from Makefile
2022-07-31 17:17:26 -04:00
Keith Grant
86a67abbce Merge pull request #12531 from jtmelhorn/devel
[#12478] Change Inventory "Status" column header to "Sync Status"
2022-07-29 15:50:08 -07:00
Keith Grant
d555093325 Fix job output follow mode & scrolling (#12555)
* reworks/fixes follow mode

* reduces batch size for better job output perceived performance

* improves job output scroll button behavior
2022-07-28 15:26:25 -04:00
John Westcott IV
95a099acc5 Adding remove_superuser and remove_system_auditors to the SAML user attribute map (#12522) 2022-07-28 14:38:16 -04:00
John Westcott IV
d1fc2702ec Adding subscriptions module and adding pool_id to license module (#12560) 2022-07-28 12:16:47 -04:00
Alan Rominger
3aa8320fc7 Add a graph to show database connections being used 2022-07-28 11:52:36 -04:00
John Westcott IV
734899228b Updating CONTRIBUTING guide (#12565) 2022-07-27 09:59:09 -04:00
Rick Elrod
87f729c642 [FieldLookupBackend] limit iexact to string fields (#12569)
Change:
- Case-insensitive search only makes sense on strings, so check the
  type of the field we are searching and ensure it is a string field
  (TextField, CharField, or some subclass thereof).

- This prevents a 500 error when a user uses iexact on, e.g., an
  integer field. Now, a 400 Bad Request is returned instead.

Test Plan:
- Added simple unit tests for iexact

Tickets:
- Fixes #9222

Signed-off-by: Rick Elrod <rick@elrod.me>
2022-07-26 12:46:50 -05:00
John Westcott IV
62fc3994fb Modifying SAML adapter to not auto-add default galaxy creds to orgs on login (#12504)
* Modifying SAML adapter to not auto-add default galaxy creds to orgs on login

* Adding test, fixing old tests and moving add_default_galaxy_credential to pipeline
2022-07-25 17:16:22 -03:00
Shane McDonald
0d097964be Automatically install setuptools-scm in script called from Makefile 2022-07-22 12:59:39 -04:00
Christian Adams
9f8b3948e1 Merge pull request #12147 from rooftopcellist/bump-receptor-1.2.3
Bump Receptorctl to 1.2.3
2022-07-21 11:45:27 -04:00
Jessica Steurer
1ce8240192 Merge pull request #12528 from vedaperi/12436-RemoveUpdateOnProjectUpdate
Remove update_on_project_update
2022-07-20 16:14:23 -03:00
Jeff Bradberry
1bcfc8f28e Merge pull request #12544 from jbradberry/awxkit-fix-no-content
Suppress 204 No Content results causing an error during import
2022-07-20 10:48:02 -04:00
vedaperi
71925de902 Enhanced detail component (#12432)
* Enhanced detail component to handle cases with no values, and refactored components that use detail component.

* Add optional chaining operators where necessary to pass test cases

* add test cases to test suites of modified files

Co-authored-by: Veda Periwal <vperiwal@vperiwal-mac.attlocal.net>
2022-07-19 17:17:27 -04:00
Aditya Mulik
54057f1c80 Merge pull request #12467 from adityamulik/localization_scripts
Localization Scripts for AWX UI & API
2022-07-19 16:40:10 -04:00
Aditya Mulik
ae388d943d Merge pull request #12541 from adityamulik/translations_updated_2022-07-18_20_51_59
Pushing updated strings for localization
2022-07-19 16:39:44 -04:00
Alan Rominger
2d310dc4e5 Optimize object creation by getting fewer empty relationships (#12508)
This optimizes the ActivityStreamSerializer by only getting many-to-many
  relationships that are speculatively non-empty
  based on information we have in other fields

We run this every time we create an object as an on_commit action
  so it is expected this will have a major impact on response times for launching jobs
2022-07-19 14:27:51 -04:00
Jeff Bradberry
fe1a767f4f Suppress 204 No Content results causing an error during import 2022-07-19 12:25:24 -04:00
adityamulik
8c6581d80a Pushing updated strings for localization 2022-07-18 20:52:59 -04:00
Jessica Steurer
33e445f4f6 Merge pull request #12489 from kialam/vendor-d3.js-webworker
Remove external script call to D3.js.
2022-07-18 19:10:50 -03:00
Kia Lam
9bcb60d9e0 Remove d3 csp declaration. 2022-07-18 08:57:03 -07:00
Kia Lam
40109d58c7 Host d3 files needed for webworker. 2022-07-18 08:57:02 -07:00
Kia Lam
2ef3f5f9e8 Remove external script call to D3.js. 2022-07-18 08:57:02 -07:00
John Westcott IV
389c4a3180 Adding fields to job_metadata for workflows and approval nodes (#12255) 2022-07-18 16:53:49 +02:00
Justin Melhorn
bee48671cd [#12478] Change Inventory "Status" column header to "Sync Status"
Signed-off-by: Justin Melhorn <jtmelhorn@gmail.com>
2022-07-17 16:38:24 -04:00
Veda Periwal
21f551f48a Remove update_on_project_update from inventory sources form and corresponding files 2022-07-15 11:18:16 -07:00
Alex Corey
cbb019ed09 Merge pull request #12510 from AlexSCorey/11822-JobOutputDocumentation-Overview
Adds Overview of job output with some images to help.
2022-07-15 10:52:47 -04:00
Alex Corey
bf5dfdaba7 Adds Overview of job output with some images to help. 2022-07-15 10:32:41 -04:00
Jessica Steurer
0f7f8af9b8 Merge pull request #12346 from john-westcott-iv/dependabot_fixes
Updating pyjwt per dependabot
2022-07-15 10:42:24 -03:00
Sarabraj Singh
0237402390 Merge pull request #12509 from sarabrajsingh/docs/awx-release-docs-refactoring
buffed docs for awx release and canonical triage responses
2022-07-15 08:21:58 -04:00
Hao Liu
84d7fa882d Merge pull request #12513 from TheRealHaoLiu/fix-workflow-job-template-export
fix WorkflowJobTemplate export
2022-07-14 14:44:58 -04:00
Sarabraj Singh
cd2fae3471 buffed docs for AWX Release and canonical Triage responses 2022-07-14 14:13:18 -04:00
John Westcott IV
8be64145f9 Updating pyjwt per dependabot 2022-07-14 08:35:46 -04:00
djyasin
23d28fb4c8 Merge pull request #12457 from djyasin/feature/bu-metrics-added-forks-in-unified-jobs-table
Added forks to unified jobs table.
2022-07-13 11:33:19 -04:00
Lila
aeffd6f393 Bumped up version number of the collector. 2022-07-13 09:59:41 -04:00
djyasin
ab6b4bad03 Merge branch 'ansible:devel' into devel 2022-07-13 09:53:22 -04:00
Hao Liu
769c253ac2 fix WorkflowJobTemplate export where WorkflowApprovalTemplate is not properly exported
fixes https://github.com/ansible/awx/issues/7946
- added WorkflowApprovalTemplate page type to allow URL registration
- added resources regex that’s associated resource URL with WorkflowApprovalTemplate
- registered the new resource regex with WorkflowApprovalTemplate page type
- modified `DEPENDENT_EXPORT` handling (insisted by @jbradberry)
- added special case handling for WorkflowApprovalTemplate due to its unique nature

unique nature of WorkflowApprovalTemplate
- when exporting WorkflowJobTemplate with approval node the WorkflowJobTemplateNode need to contain a related "create_approval_template" the POST data for "create_approval_template" need to come from the "workflow_approval_template"
- during the export of a WorkflowJobTemplateNode that is an approval node we need to get the data from "workflow_approval_template" and use that to populate the "create_approval_template"

Co-Authored-By: Jeff Bradberry <685957+jbradberry@users.noreply.github.com>
Signed-off-by: Hao Liu <haoli@redhat.com>
2022-07-12 19:48:02 -04:00
Michael Abashian
8031b3d402 Translate contents of Hosts Automated field as a single string (#12480)
* Translate contents of Hosts Automated field as a single string

* Adds unit test case for hiding Hosts automated detail when no value is present
2022-07-12 15:24:33 -04:00
Sarabraj Singh
bd93ac7edd Merge pull request #12505 from sarabrajsingh/bugfix/add-setuptools-scm-dependency-to-workflow
added setuptools-scm dependency to promote.yml workflow
2022-07-12 10:21:10 -04:00
John Westcott IV
37ff9913d3 Adding GOOGLE_APPLICATION_CREDENTIALS env var (#12389)
* Adding GOOGLE_APPLICATION_CREDENTIALS env var
* Updating tests
2022-07-12 08:51:02 -04:00
Sarabraj Singh
9cb44a7e52 added setuptools-scm dependency to promote.yml workflow 2022-07-11 17:10:29 -04:00
John Westcott IV
6279295541 Updating workflow job template collection test (#12468)
Adding additional use case

Fixing error with workflow calling itslef

Adding better cleanup of assets created as part of the test
2022-07-11 17:07:07 -03:00
John Westcott IV
de17cff39c Modified triage replied (#12473)
Split no progress into issue and pr

added community.general standard response
2022-07-11 12:43:30 -04:00
Alex Corey
22ca49e673 Merge pull request #12493 from AlexSCorey/bumpCodeMirror
Bump code mirror
2022-07-11 09:43:54 -04:00
Tom Page
008a4b4d30 Fix workflow job template webhook credential bug - #12324 (#12325)
Signed-off-by: tompage1994@hotmail.co.uk <tpage@redhat.com>
2022-07-11 09:13:44 -03:00
Alex Corey
8d4089c7f3 Bumps code mirror and adds license files 2022-07-08 15:09:54 -04:00
vedaperi
e296d0adad Add Help Text with documentation link to Schedules page (#12448)
* Added help text to schedule form and detail with link to documentation

* Added test cases for help text in schedule form and detail

* Add help text to schedule form and detail with link to documentation

Co-authored-by: Veda Periwal <vperiwal@vperiwal-mac.attlocal.net>
2022-07-08 15:06:50 -04:00
Aditya Mulik
df38650aee Localization Scripts for AWX UI & API 2022-07-08 11:44:56 -04:00
Alex Corey
401b30b3ed Merge pull request #12451 from ansible/dependabot/npm_and_yarn/awx/ui/devel/patternfly/patternfly-4.202.1
Bump @patternfly/patternfly from 4.196.7 to 4.202.1 in /awx/ui
2022-07-08 08:13:30 -04:00
Alex Corey
20cc54694c Merge pull request #12454 from ansible/dependabot/npm_and_yarn/awx/ui/devel/patternfly/react-icons-4.75.1
Bump @patternfly/react-icons from 4.49.19 to 4.75.1 in /awx/ui
2022-07-08 08:12:58 -04:00
dependabot[bot]
e6ec0952fb Bump @patternfly/patternfly from 4.196.7 to 4.202.1 in /awx/ui
Bumps [@patternfly/patternfly](https://github.com/patternfly/patternfly) from 4.196.7 to 4.202.1.
- [Release notes](https://github.com/patternfly/patternfly/releases)
- [Changelog](https://github.com/patternfly/patternfly/blob/main/RELEASE-NOTES.md)
- [Commits](https://github.com/patternfly/patternfly/compare/prerelease-v4.196.7...prerelease-v4.202.1)

---
updated-dependencies:
- dependency-name: "@patternfly/patternfly"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-06 20:03:52 +00:00
dependabot[bot]
db1dec3a98 Bump @patternfly/react-icons from 4.49.19 to 4.75.1 in /awx/ui
Bumps [@patternfly/react-icons](https://github.com/patternfly/patternfly-react) from 4.49.19 to 4.75.1.
- [Release notes](https://github.com/patternfly/patternfly-react/releases)
- [Commits](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-icons@4.49.19...@patternfly/react-icons@4.75.1)

---
updated-dependencies:
- dependency-name: "@patternfly/react-icons"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-06 20:03:40 +00:00
Alex Corey
1853d3850e Merge pull request #12450 from ansible/dependabot/npm_and_yarn/awx/ui/devel/patternfly/react-table-4.93.1
Bump @patternfly/react-table from 4.83.1 to 4.93.1 in /awx/ui
2022-07-06 16:02:18 -04:00
Lila
1e57c84383 Added forks to unified jobs table.
Co-authored-by: sarabrajsingh <singh.sarabraj@gmail.com>
2022-07-01 10:30:48 -04:00
dependabot[bot]
3cf120c6a7 Bump @patternfly/react-table from 4.83.1 to 4.93.1 in /awx/ui
Bumps [@patternfly/react-table](https://github.com/patternfly/patternfly-react) from 4.83.1 to 4.93.1.
- [Release notes](https://github.com/patternfly/patternfly-react/releases)
- [Commits](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-table@4.83.1...@patternfly/react-table@4.93.1)

---
updated-dependencies:
- dependency-name: "@patternfly/react-table"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-01 08:06:59 +00:00
Alan Rominger
fd671ecc9d Give specific messages if job was killed due to SIGTERM or SIGKILL (#12435)
* Reap jobs on dispatcher startup to increase clarity, replace existing reaping logic

* Exit jobs if receiving SIGTERM signal

* Fix unwanted reaping on shutdown, let subprocess close out

* Add some sanity tests for signal module

* Add a log for an unhandled dispatcher error

* Refine wording of error messages

Co-authored-by: Elijah DeLee <kdelee@redhat.com>
2022-06-30 13:20:08 -04:00
Shane McDonald
a0d5f1fb03 Merge pull request #12428 from djyasin/updating_setuppy
Updated setup.py --version to python3 -m setuptools_scm.
2022-06-30 12:17:54 -04:00
Alex Corey
ff882a322b Merge pull request #12412 from AlexSCorey/11994-FailedJobErrorMessage
Adds a failure message to job output when job failed and no events exist
2022-06-29 11:40:44 -04:00
Tom Page
b70231f7d0 Allow modification of schedule if there are two of the same name (#12407) 2022-06-28 20:23:54 -03:00
Alex Corey
93d1aa0a9d Adds a failure message to job output when job failed and no events exist. 2022-06-28 18:30:37 -04:00
Alex Corey
c586f8bbc6 Removes references to Ansible Tower in favor of Ansible Controller (#12422) 2022-06-28 14:35:32 -04:00
Alex Corey
26912a06d1 Merge pull request #12424 from AlexSCorey/11433-UpdateLaunchButtonTest
Updates irrelevant test
2022-06-28 14:31:26 -04:00
Alex Corey
218a3d333b updates test 2022-06-28 14:14:12 -04:00
Seth Foster
d2013bd416 Merge pull request #12366 from fosterseth/remove_update_on_project_update
Remove deprecated field update_on_project_update
2022-06-28 13:15:57 -04:00
Shane McDonald
6a3f9690b0 Remove setup.py entirely 2022-06-27 14:15:32 -04:00
Jeff Bradberry
d59b6f834c Merge pull request #12431 from jbradberry/fix-ugettext-deprecation
Fix a ugettext deprecation that snuck back in
2022-06-27 13:58:07 -04:00
Shane McDonald
cbea36745e Transition from setup.py to setup.cfg 2022-06-27 13:30:01 -04:00
Jeff Bradberry
ae7be525e1 Fix a ugettext deprecation that snuck back in
at some point after the Django 3.2 upgrade.
2022-06-27 13:27:35 -04:00
jainnikhil30
5062ce1e61 add database connection to the metrics endpoint (#12427)
* add database connection to the metrics endpoint

* bump the counts collector version to 1.2

* check for postgresql as database so to not break the tests
2022-06-27 09:37:23 -04:00
Alex Corey
566665ee8c Merge pull request #12417 from ansible/dependabot/npm_and_yarn/awx/ui/devel/patternfly/react-core-4.221.3
Bump @patternfly/react-core from 4.214.1 to 4.221.3 in /awx/ui
2022-06-27 09:36:58 -04:00
Alex Corey
96423af160 Merge pull request #12419 from ansible/dependabot/npm_and_yarn/awx/ui/devel/react-router-dom-5.3.3
Bump react-router-dom from 5.2.0 to 5.3.3 in /awx/ui
2022-06-27 09:36:22 -04:00
Alex Corey
a01bef8d2c Merge pull request #12420 from ansible/dependabot/npm_and_yarn/awx/ui/devel/lingui/react-3.14.0
Bump @lingui/react from 3.13.3 to 3.14.0 in /awx/ui
2022-06-27 09:35:40 -04:00
Seth Foster
0522233892 remove update_on_project_update from InventorySource 2022-06-24 15:27:08 -04:00
Lila
63ea6bb5b3 Updated setup.py --version to python3 -m setuptools_scm. 2022-06-24 10:22:56 -04:00
Sarah Akus
c2715d7c29 Merge pull request #12378 from john-westcott-iv/winrm_debug_5925
Making verbosity list and options a constant and adding WinRM debug
2022-06-24 09:06:14 -04:00
Alan Rominger
783b744bdb Pass combined artifacts from nested workflows into downstream nodes (#12223)
* Track combined artifacts on workflow jobs

* Avoid schema change for passing nested workflow artifacts

* Basic support for nested workflow artifacts, add test

* Forgot that only does not work with polymorphic

* Remove incorrect field

* Consolidate logic and prevent recursion with UJ artifacts method

* Stop trying to do precedence by status, filter for obvious ones

* Review comments about sets

* Fix up bug with convergence node paths and artifacts
2022-06-23 16:54:53 -03:00
Alex Corey
f7982a0d64 Merge pull request #12421 from AlexSCorey/updateAxios
Bumps Axios and Adds license files
2022-06-23 13:07:28 -04:00
Sarabraj Singh
2147ac226e Merge pull request #12408 from sarabrajsingh/feature/new-awx-cli-import-export-error-codes
[new] bubble up an error code when something goes wrong with import/export
2022-06-23 10:58:14 -04:00
Alex Corey
6cc22786bc Adds license files 2022-06-23 09:26:34 -04:00
dependabot[bot]
861a9f581e Bump @lingui/react from 3.13.3 to 3.14.0 in /awx/ui
Bumps [@lingui/react](https://github.com/lingui/js-lingui) from 3.13.3 to 3.14.0.
- [Release notes](https://github.com/lingui/js-lingui/releases)
- [Changelog](https://github.com/lingui/js-lingui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lingui/js-lingui/compare/v3.13.3...v3.14.0)

---
updated-dependencies:
- dependency-name: "@lingui/react"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-23 12:34:58 +00:00
dependabot[bot]
e57a8183ba Bump react-router-dom from 5.2.0 to 5.3.3 in /awx/ui
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 5.2.0 to 5.3.3.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Commits](https://github.com/remix-run/react-router/commits/v5.3.3/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-23 12:34:20 +00:00
dependabot[bot]
8a7163ffad Bump @patternfly/react-core from 4.214.1 to 4.221.3 in /awx/ui
Bumps [@patternfly/react-core](https://github.com/patternfly/patternfly-react) from 4.214.1 to 4.221.3.
- [Release notes](https://github.com/patternfly/patternfly-react/releases)
- [Commits](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-core@4.214.1...@patternfly/react-core@4.221.3)

---
updated-dependencies:
- dependency-name: "@patternfly/react-core"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-23 12:32:57 +00:00
Alex Corey
439b351c95 Merge pull request #12392 from nixocio/update_bot_user
Update user dependabot
2022-06-23 08:31:16 -04:00
Alex Corey
14afab918e Creates a verbosity select dropdowns and moves options constant into same file 2022-06-23 08:28:37 -04:00
Alex Corey
ef8d4e73ae Creates a verbosity select dropdowns and moves options constant into same file 2022-06-22 14:04:12 -04:00
John Westcott IV
61f483ae32 Fixing UI general test 2022-06-22 14:04:12 -04:00
John Westcott IV
21bed7473d Making verbosity list and options a constant and adding WinRM debug to everything 2022-06-22 14:04:11 -04:00
John Westcott IV
31d8ddcf84 Updating release docs (#12403)
Adding standard subject line to triage_replies.md

Removing PR commit generated change log in favor of github auto-commit log

Updating some images

Adding AWX matrix chanel to IRC notifications

Adding references between operator and AWX releases
2022-06-22 12:36:54 -04:00
Seth Foster
9419270897 Merge pull request #12393 from fosterseth/subsystem_metrics_delete_redis_keys
Subsystem metrics reset_values should remove all redis keys
2022-06-22 11:34:20 -04:00
Alex Corey
f755d93a58 Merge pull request #12373 from AlexSCorey/updateJS-Yaml
Updates js-yaml to 4.x and updates files.
2022-06-22 11:25:52 -04:00
Sarabraj Singh
05df2ebad2 bubble up an error code when something goes wrong with import/export
(cherry picked from commit babd6f0975)
2022-06-22 10:29:01 -04:00
Jeff Bradberry
b44442c460 Merge pull request #12351 from AlexSCorey/5673-t-importExportSchedules
Adds import export to awx cli for schedules as a top level object
2022-06-22 10:13:56 -04:00
Shane McDonald
989b389ba4 Merge pull request #12397 from sean-m-sullivan/awx_license_delete
add state to awx license module
2022-06-22 09:20:29 -04:00
Sarabraj Singh
5bd4aade0e Merge pull request #12404 from ansible/revert-12335-feature/awx-cli-import-export-error-codes
Revert "import/export error codes when something bad happens"
2022-06-21 22:01:46 -04:00
Jessica Steurer
470910b612 Merge pull request #12309 from jbradberry/cli-multiple-extra-vars
Allow for multiple --extra_vars or --variables flags in awx-cli
2022-06-21 19:34:25 -03:00
Sarabraj Singh
dbb81551c8 Revert "import/export error codes when something bad happens" 2022-06-21 17:36:21 -04:00
Sarabraj Singh
f7c5cb2979 Merge pull request #12335 from sarabrajsingh/feature/awx-cli-import-export-error-codes
import/export error codes when something bad happens
2022-06-21 16:49:03 -04:00
Sarabraj Singh
babd6f0975 bubble up an error code when something goes wrong with import/export 2022-06-21 15:53:59 -04:00
sean-m-sullivan
7bcceb7e98 add state to awx license module 2022-06-21 13:07:16 -04:00
Seth Foster
c92619a2dc Subsystem metrics reset_values should remove all redis keys 2022-06-16 16:54:37 -04:00
Alan Rominger
923cc671db Merge pull request #12391 from AlanCoding/compose_graphs
Do the grafana thing in docker-compose templating itself
2022-06-16 16:23:36 -04:00
Alan Rominger
db105c21e4 Set default false values 2022-06-16 15:46:42 -04:00
Alan Rominger
372aa36207 Make the prometheus config file ignored by git 2022-06-16 15:42:10 -04:00
Alan Rominger
173318764b Remove existing yml file for prometheus 2022-06-16 15:37:18 -04:00
Alan Rominger
1dd535a859 Remove old way of doing grafana graphs 2022-06-16 15:31:45 -04:00
nixocio
e7d37b26f3 Update user dependabot
Update user dependabot
2022-06-16 15:31:39 -04:00
Alan Rominger
f4ef7d6927 Add volumes to the clean command 2022-06-16 14:03:22 -04:00
Elijah DeLee
7cbe112e4e possible work around for 500 on /api/v2/metrics (#12376)
we've observed this in development and some users have reported experiencing 500's on /api/v2/metrics because of a key error here where a metric is missing from a certain instance
2022-06-16 13:15:25 -04:00
Alan Rominger
c441db2aab docs workding edits and depends_on 2022-06-16 12:07:26 -04:00
Alan Rominger
fb292d9706 Move visualization containers into docker-compose 2022-06-16 10:25:02 -04:00
Sarah Akus
35a5f93182 Merge pull request #12323 from AlexSCorey/5857-t-SanitizeLoginHTML
Removes Sanatize html in favor of dom purify library
2022-06-16 09:59:21 -04:00
Jessica Steurer
116dc0c480 Merge pull request #12340 from john-westcott-iv/shedule_timezone_12255
Add documentation around schedule timezone change
2022-06-15 15:34:49 -03:00
Alex Corey
b87ba1c53d Merge pull request #12382 from nixocio/ui_close_css
Update css var
2022-06-15 11:56:47 -04:00
Alex Corey
59691b71bb Merge pull request #12360 from nixocio/ui_issue_5012
Add column to display resource related to a schedule
2022-06-15 11:53:33 -04:00
Alex Corey
cc0bb3e401 Merge pull request #12365 from ansible/dependabot/npm_and_yarn/awx/ui/devel/ace-builds-1.6.0
Bump ace-builds from 1.5.1 to 1.6.0 in /awx/ui
2022-06-15 11:46:53 -04:00
nixocio
7ef90bd9f4 Update css var
Update css var
2022-06-15 11:37:04 -04:00
John Westcott IV
f820c49b82 Fixing typo in ISSUE_TEMPLATE.md (#12381) 2022-06-15 10:34:22 -04:00
Jessica Steurer
ac62d86f2a Merge pull request #12361 from kialam/refresh-data-lookup-modal
Allow lookup modals to refresh when opened.
2022-06-15 09:40:40 -03:00
John Westcott IV
b9e67e7972 Allowing blank issues with a template for testing purposes only (#12377) 2022-06-14 17:17:07 -04:00
Jeff Bradberry
48a2ebd48c Merge pull request #12271 from HampusLundqvist/gitlab-webhooks-fixes-#12268
return event_status on push, tag push, and merge gitlab webhook events
2022-06-14 17:12:27 -04:00
Sarah Akus
ee13ddd87d Merge pull request #12332 from nixocio/ui_issue_8097
Add typeahed for single choice surveys
2022-06-14 15:20:38 -04:00
Seth Foster
3fcf7429a3 Merge pull request #12246 from fosterseth/fix_haproxy_startup_error
use haproxy 2.3 with maxconn set to avoid startup failures
2022-06-14 14:41:14 -04:00
Sarah Akus
51a8790d56 Merge pull request #12348 from nixocio/ui_issue_111987
Update project status to reflect project sync related to job template
2022-06-14 14:41:01 -04:00
Jessica Steurer
c231e4d05e Merge pull request #12370 from nixocio/ui_issue_11795
Add column org to template list
2022-06-14 14:28:56 -03:00
Seth Foster
987e5a084d use haproxy 2.3 with maxconn set to avoid startup failures 2022-06-14 13:09:40 -04:00
Seth Foster
70ac7b2920 Merge pull request #12352 from fosterseth/docs_subsystem_metrics
Add docs for subsystem metrics
2022-06-14 13:05:21 -04:00
Alex Corey
bda335cb19 Updates js-yaml to 4.x and updates files. 2022-06-14 12:24:40 -04:00
Seth Foster
30c060cb27 Merge pull request #12235 from fosterseth/subsystem_metrics_task_manager
Subsystem metrics for task manager
2022-06-14 12:02:54 -04:00
Kersom
9b0a2b0b76 Merge pull request #12312 from nixocio/ui_issue_11167_rebased
Update logout/login redirect for different users
2022-06-14 11:55:05 -04:00
Seth Foster
2f82b75748 Add subsystem metrics for task manager 2022-06-14 11:00:11 -04:00
Sarah Akus
84fcd2ff00 Merge pull request #12363 from nixocio/ui_issue_5195
Modify position of tooltip for management job list
2022-06-14 10:29:49 -04:00
Jeff Bradberry
3bc0c53e37 Merge pull request #12368 from jbradberry/narrower-autoreload
Narrow down the inotifywait criteria for reloading the dev environment
2022-06-14 10:13:41 -04:00
Alex Corey
bc2dbcfce8 Merge pull request #12344 from ansible/dependabot/npm_and_yarn/awx/ui/devel/patternfly/patternfly-4.196.7
Bump @patternfly/patternfly from 4.194.4 to 4.196.7 in /awx/ui
2022-06-13 16:58:48 -04:00
nixocio
876edf54a3 Modify position of tooltip for management job list
Modify position of tooltip for management job list. Also, remove
duplicated tooltip.
2022-06-13 16:42:43 -04:00
nixocio
b31bf8fab1 Add column org to template list
Add column org to template list

See: https://github.com/ansible/awx/issues/11795
2022-06-13 16:37:32 -04:00
Jeff Bradberry
e8b2998578 Narrow down the inotifywait criteria for reloading the dev environment
- listen specifically within awx/awx, so that changes in awxkit or
  awx_collection don't trigger spurious reloads
- expand the exclude pattern to ignore the test directories
2022-06-13 16:08:20 -04:00
nixocio
8a92a01652 Add column to display resource related to a schedule
Add column to display what resource is related to a schedule

See: https://github.com/ansible/awx/issues/5012
2022-06-13 14:28:44 -04:00
Seth Foster
705f86f8cf Merge pull request #12287 from fosterseth/fix_children_summary_not_tree
detect if job events are tree-like and collapsible
2022-06-13 14:27:39 -04:00
Alex Corey
9ab6a6d57e Merge pull request #11429 from akelling/patch-1
Update README.md
2022-06-13 14:19:16 -04:00
Sarah Akus
791eb4c1e1 Merge pull request #12349 from nixocio/ui_issue_12092
Add loading state when saving a visualizer
2022-06-13 14:06:34 -04:00
dependabot[bot]
870ca29388 Bump ace-builds from 1.5.1 to 1.6.0 in /awx/ui
Bumps [ace-builds](https://github.com/ajaxorg/ace-builds) from 1.5.1 to 1.6.0.
- [Release notes](https://github.com/ajaxorg/ace-builds/releases)
- [Changelog](https://github.com/ajaxorg/ace-builds/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ajaxorg/ace-builds/compare/v1.5.1...v1.6.0)

---
updated-dependencies:
- dependency-name: ace-builds
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-13 18:00:10 +00:00
Kersom
816518cfab Merge pull request #12302 from ansible/dependabot/npm_and_yarn/awx/ui/devel/react-ace-10.1.0
Bump react-ace from 9.4.0 to 10.1.0 in /awx/ui
2022-06-13 13:58:55 -04:00
Alex Corey
9e981583a6 Merge branch 'devel' into patch-1 2022-06-13 13:55:02 -04:00
Alex Corey
d6fb8d6cd7 Update tools/docker-compose/README.md
Co-authored-by: Shane McDonald <me@shanemcd.com>
2022-06-13 13:53:48 -04:00
Sarah Akus
7dbf5f7138 Merge pull request #12358 from nixocio/ui_issue_5883
Hide add access button based on the user profile for credentials
2022-06-13 13:38:36 -04:00
dependabot[bot]
aaec9487e6 Bump react-ace from 9.4.0 to 10.1.0 in /awx/ui
Bumps [react-ace](https://github.com/securingsincity/react-ace) from 9.4.0 to 10.1.0.
- [Release notes](https://github.com/securingsincity/react-ace/releases)
- [Changelog](https://github.com/securingsincity/react-ace/blob/main/CHANGELOG.md)
- [Commits](https://github.com/securingsincity/react-ace/compare/v9.4.0...v10.1.0)

---
updated-dependencies:
- dependency-name: react-ace
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-13 17:37:54 +00:00
Kia Lam
96fa881df1 Fix unit test. 2022-06-13 08:59:31 -07:00
Seth Foster
b7057fdc3e Add docs for subsystem metrics 2022-06-13 11:49:56 -04:00
nixocio
2679c99cad Add loading state when saving a visualizer
Add loading state when saving a visualizer

See: https://github.com/ansible/awx/issues/12092
2022-06-13 10:47:27 -04:00
Jessica Steurer
ea3a8d4912 Merge pull request #12306 from ansible/10961-webhook-notification-does-not-allow-for-use-of-jinja-statements
Duplication of PR of Jinga 2 Rendering
2022-06-13 09:38:42 -03:00
John Westcott IV
63d9cd7b57 .github folder maintaince (#12327)
* Removing old awxbot files
* Removing security bug report as GitHub now shows the security piolicy from /SECURITY.md
* Changing feature_request from md to yml
* Adding additional options to bug report components andinstall method
* Removing old ISSUE_TEMPLATE.md
* Changing issue type and adding additional components
* Removing auto-generated change log
* Adding awx_collection and cli components
* Changing content search pattern for type labels
* Changing from collection to awx_collection tag and adding dependencies tag
* Adding unicode bug to bug repot to match feature unicode character
* Changing bug to bug or docs
* Remove docker on * and boot2docker infavor of docker development environmnet
* Create top level issue with: CoC, Enterprise, Top level help
* Remove old CODEOWNERS file
2022-06-13 07:44:15 -04:00
Kia Lam
b692bbaa12 Allow lookup modals to refresh when opened. 2022-06-10 14:44:53 -07:00
John Westcott IV
186af73e5d Fixing slashes for copy/paste of links (#12359) 2022-06-10 14:29:12 -04:00
John Westcott IV
fddf292d47 Additional changes from review 2022-06-10 10:26:24 -04:00
John Westcott IV
1180634ba7 Fixing UI checks 2022-06-10 10:26:23 -04:00
John Westcott IV
9abdafe101 Removing read_only as its the default setting 2022-06-10 10:26:23 -04:00
John Westcott IV
48ebcd5918 Fixing assertion of schedule_zoneinfo 2022-06-10 10:26:23 -04:00
John Westcott IV
fe6d0ce9cc Adding help text to until and timezone fields 2022-06-10 10:26:23 -04:00
John Westcott IV
62dabcae63 Removing unneeded function 2022-06-10 10:26:23 -04:00
Keith J. Grant
0b63af8d4d add schedules timezone link warning to UI 2022-06-10 10:26:23 -04:00
John Westcott IV
b05ebe9623 Starting UI change to warn if linked TZ is selected 2022-06-10 10:26:23 -04:00
John Westcott IV
c836fafb61 modifying schedules API to return a list of links 2022-06-10 10:26:23 -04:00
nixocio
96330f608d Hide add access based on the user profile for credentials
* Show add access button if it is a system admin
* Hide access button if the user is credential admin, org admin, but the
  credential does not belong to any org.
* Show access button if the user is a credential admin, org admin, and
  the credential is associated to an org.
* Show access button if the user is an org admin and the credential is
  associated to the org.

All those permutations are allowed by the API RBAC.
This PR update UX to not allow the user to attempt to perform any
action that will raise an error when modifying access to the
credentials.
2022-06-10 10:09:18 -04:00
Kersom
23aaf5b3ad Add cancel button to workflow job output (#12338)
Add cancel button to workflow job output

See: https://github.com/ansible/awx/issues/10514
2022-06-09 20:16:07 -04:00
Kersom
a3e86dcd73 Hide management job for non system admin as node choice (#12341)
Hide management job for non system admin as node type choice. Also, fix
related uni-tests related to this change.

See: https://github.com/ansible/awx/issues/12334
Also: https://github.com/ansible/awx/pull/10572
2022-06-09 20:15:03 -04:00
Alan Rominger
81b8028ea2 Merge pull request #12355 from AlanCoding/autoreload_once
Make awx-autoreloader work faster for large code changes
2022-06-09 15:19:17 -04:00
Alan Rominger
a4bfb032ff Make awx-autoreloader work faster for large code changes 2022-06-09 14:52:03 -04:00
Keith J. Grant
2704b202bf check for is_tree flag from children summary response 2022-06-09 14:25:39 -04:00
Seth Foster
550d9d5e42 detect if job events are tree-like and collapsable in the UI 2022-06-09 14:25:39 -04:00
John Westcott IV
ab2d05a07d Update replies documentation (#12305)
Adding heads and a couple standard replies and rewording other replies.
2022-06-09 13:41:53 -04:00
Alan Rominger
4543f6935f Only do substitutions for container path conversions with resolved paths (#12313)
* Resolve paths as much as possible before doing replacements

* Move unused method out of main code, test symlink
2022-06-09 11:36:29 -04:00
Alan Rominger
78d3d6dc94 Merge pull request #12219 from AlanCoding/really_skip
Change Demo Project status to successful
2022-06-09 11:19:57 -04:00
Alex Corey
02e7424f51 Adds import export to awx cli for schedules as a top level object 2022-06-09 09:47:50 -04:00
Andrea Decorte
2d6ca4cbb1 Update role module example (#12295)
Update example to use current parameter for workflows
instead of the deprecated one.

Signed-off-by: Andrea Decorte <adecorte@redhat.com>
2022-06-09 09:38:55 -04:00
Aine Riordan
e244644a1d Fix typo in application module example (#12187) 2022-06-09 09:38:34 -04:00
Jessica Steurer
d216457c09 Merge pull request #12320 from nixocio/ui_issue_2899
Pre-fill project for job template from query params
2022-06-09 10:24:29 -03:00
nixocio
20a1da61c0 Update project status to reflect project sync related to job template
Update project status to reflect project update sync related to job
template that was launched with branch override.

We were displaying status of project sync itself, not from the project
update job as expected.

Also, rename `Project Status` to be `Project Update Status`.

See: https://github.com/ansible/awx/issues/11987
2022-06-08 13:41:45 -04:00
Jessica Steurer
bf7ab1ede7 Merge pull request #12315 from djyasin/job_tag_characters
Job tag characters
2022-06-08 12:09:18 -03:00
Alex Corey
3b6b449545 Removes unneeded license files 2022-06-08 10:04:25 -04:00
Alex Corey
781cf531e6 Removes Sanatize html in favor of dom purify library 2022-06-08 10:04:25 -04:00
dependabot[bot]
9b7475247c Bump @patternfly/patternfly from 4.194.4 to 4.196.7 in /awx/ui
Bumps [@patternfly/patternfly](https://github.com/patternfly/patternfly) from 4.194.4 to 4.196.7.
- [Release notes](https://github.com/patternfly/patternfly/releases)
- [Changelog](https://github.com/patternfly/patternfly/blob/main/RELEASE-NOTES.md)
- [Commits](https://github.com/patternfly/patternfly/compare/prerelease-v4.194.4...prerelease-v4.196.7)

---
updated-dependencies:
- dependency-name: "@patternfly/patternfly"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-08 14:00:52 +00:00
Alex Corey
44dc7f8d1d Merge pull request #12333 from ansible/dependabot/npm_and_yarn/awx/ui/devel/rrule-2.7.0
Bump rrule from 2.6.4 to 2.7.0 in /awx/ui
2022-06-08 09:59:39 -04:00
Kersom
60eaf9e235 Provide feedback when a health check is being performed (#12330)
Provide feedback when a health check is being performed
2022-06-07 16:27:46 -04:00
Jessica Steurer
f5102ed24d Merge pull request #12102 from john-westcott-iv/allow_fqcn
Respect optional fully qualified collection name (ansible.builtin.) for playbook identification
2022-06-07 16:44:36 -03:00
Jessica Steurer
309178e4e2 Merge pull request #12331 from kialam/fix-worker-json-404
Allow worker files to be loaded as blob objects.
2022-06-07 16:33:59 -03:00
Rebeccah Hunter
76ffdbb993 Merge pull request #12308 from rebeccahhh/job_event_lag
Metrics for callback receiver job event lag
2022-06-07 11:50:17 -04:00
nixocio
d8037618c8 Update logout/login redirect for different users
* Logout as User A and Login as User B redirects to `/home'
* Logout as User A and Login as User A redirects to `/home'
* Allow session to timeout as User A and Login as User A redirects to User A's last location

See: https://github.com/ansible/awx/issues/11167
2022-06-07 09:48:41 -04:00
Alex Corey
e94e15977c Merge pull request #12328 from ansible/dependabot/npm_and_yarn/awx/ui/async-2.6.4
Bump async from 2.6.3 to 2.6.4 in /awx/ui
2022-06-07 09:13:47 -04:00
John Westcott IV
f37951249f Adding options fqcn (ansible.builtin.) to playbook identification 2022-06-06 17:32:37 -04:00
Jeff Bradberry
9191079dda Merge pull request #11921 from jbradberry/fix-export-reconstruct-endpoint
Look up the correct top-level resource name when reconstructing foreign keys
2022-06-06 17:08:02 -04:00
Keith Grant
fdd560747d Persistent list filters (#12229)
* add PersistentFilters component

* add PersistentFilters test

* add persistent filters to all list pages

* update tests

* clear sessionStorage on logout

* fix persistent filter on wfjt detail; cleanup
2022-06-06 16:56:45 -04:00
Jeff Bradberry
faa5df19ca Merge pull request #12252 from jbradberry/fix-analytics-unicode
Double escape all unicode escape sequences in job events data
2022-06-06 16:41:06 -04:00
Rebeccah
5f9326b131 added average event processing metric (in seconds) that can be served to
grafana via prometheus.

This metric is a good indicator of how far behind the callback receiver
is. The higher the load the further behind/the greater the number of
seconds the metric will display.

This number being high may indicate the need for horizontal scaling in
the control plane or vertically scaling the number of callback
receivers.
2022-06-06 15:14:56 -04:00
dependabot[bot]
8e389d40b4 Bump rrule from 2.6.4 to 2.7.0 in /awx/ui
Bumps [rrule](https://github.com/jakubroztocil/rrule) from 2.6.4 to 2.7.0.
- [Release notes](https://github.com/jakubroztocil/rrule/releases)
- [Changelog](https://github.com/jakubroztocil/rrule/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jakubroztocil/rrule/commits)

---
updated-dependencies:
- dependency-name: rrule
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-06 18:58:46 +00:00
nixocio
e62c77e783 Add typeahed for single choice surveys
Add typeahed for single choice surveys, also fix a couple of missing
translations for Select component.

See: https://github.com/ansible/awx/issues/8097
2022-06-06 13:57:00 -04:00
Kia Lam
48b3a43ec2 Allow worker files to be loaded as blob objects. 2022-06-06 10:47:30 -07:00
Lila
5f783fd5ee Revised job_tags to handle more than 1024 characters. 2022-06-06 13:28:22 -04:00
dependabot[bot]
e112cf93c2 Bump async from 2.6.3 to 2.6.4 in /awx/ui
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-06 13:51:52 +00:00
Alex Corey
d9f26a411e Merge pull request #12318 from ansible/dependabot/npm_and_yarn/awx/ui/node-forge-1.3.1
Bump node-forge from 1.2.1 to 1.3.1 in /awx/ui
2022-06-05 14:25:42 -04:00
Kersom
ea84e7a491 Merge pull request #12322 from nixocio/fix_typo
Fix typo
2022-06-03 22:46:06 -04:00
Alex Corey
7fab619fed Merge pull request #12317 from ansible/dependabot/npm_and_yarn/awx/ui/ejs-3.1.8
Bump ejs from 3.1.6 to 3.1.8 in /awx/ui
2022-06-03 16:13:35 -04:00
nixocio
699a35b88a Fix typo
Fix typo on triage replies
2022-06-03 15:22:49 -04:00
nixocio
8095adb945 Pre-fill project for job template from query params
Pre-fill project when creating JT from Project -> Job Templates
List
2022-06-03 11:32:01 -04:00
Hampus Lundqvist
8d36712860 return status on event types defined in ref_keys 2022-06-03 16:10:44 +02:00
dependabot[bot]
0db34d0498 Bump node-forge from 1.2.1 to 1.3.1 in /awx/ui
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.2.1 to 1.3.1.
- [Release notes](https://github.com/digitalbazaar/forge/releases)
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.2.1...v1.3.1)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-03 14:06:45 +00:00
dependabot[bot]
7ab254e5e3 Bump ejs from 3.1.6 to 3.1.8 in /awx/ui
Bumps [ejs](https://github.com/mde/ejs) from 3.1.6 to 3.1.8.
- [Release notes](https://github.com/mde/ejs/releases)
- [Changelog](https://github.com/mde/ejs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mde/ejs/compare/v3.1.6...v3.1.8)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-03 14:06:14 +00:00
Alex Corey
dd7ab459e2 Merge pull request #12196 from AlexSCorey/popoversInventoryAndInventorySource
Adds popover text for Inventory and InventorySources
2022-06-03 10:01:36 -04:00
Alex Corey
33df2e8aa4 Adds popover text for Inventory and InventorySources 2022-06-03 09:38:45 -04:00
Jessica Steurer
39b8fd433b Merge pull request #12251 from nixocio/ui_issue_11196
Add controller_node to job details page
2022-06-03 08:57:29 -03:00
Kersom
c31d74100d Add host description in a couple of screens (#12292)
Add host description in a couple of screens

See:https://github.com/ansible/awx/issues/3348
Also: https://github.com/ansible/awx/issues/9363
2022-06-02 15:40:41 -04:00
Alan Rominger
3af89c1e2b Merge pull request #12307 from AlanCoding/twilio
Upgrade twilio dependency to pick up fix
2022-06-02 13:48:34 -04:00
John Westcott IV
1d35bba8c3 Variablizing the awx_template_version for building to allow release process to update the version in the module_util (#12248) 2022-06-02 12:28:57 -04:00
djyasin
c3c3e24875 Merge pull request #12314 from john-westcott-iv/add_irc_msg_to_release
Adding irc bullhorn to release process
2022-06-02 11:57:32 -04:00
John Westcott IV
ab9c97b158 Adding irc bullhorn to release process 2022-06-02 11:30:57 -04:00
nixocio
5e700c992d Add controller_node to job details page
Add controller_node to job details page. Modify serializers to make
controller_node available to the UI.

See: https://github.com/ansible/awx/issues/11196
Also: https://github.com/ansible/awx/issues/12132
2022-06-02 11:21:06 -04:00
Seth Foster
b548ad21a9 Merge pull request #12240 from fosterseth/make_prometheus_grafana
Add prometheus and grafana make commands for local environment
2022-06-01 17:55:43 -04:00
Jeff Bradberry
127016d36b Allow for multiple --extra_vars or --variables flags in awx-cli
This is particularly useful when you are using the @filepath version
of the flag, since otherwise there would be no way to issue the
command with multiple vars files.

Also, add `-e` as an alias to `--extra_vars`
2022-06-01 13:24:24 -04:00
kialam
3d0391173b Add popover help text to job details and ad hoc job details (#12261)
* Add popover text to Job Details page.

* Add module documentation links to ad hoc job detail page.

* Add forks help text to job details.
2022-06-01 13:00:59 -04:00
kialam
ce560bcd5f Cleanup some text strings files to return object literals (#12269)
* Cleanup some text strings files to return object literals instead of arrow functions.

* Fix render.

* Fix unit tests.
2022-06-01 12:10:55 -04:00
Alan Rominger
d553c37d7d Upgrade twilio dependency to pick up fix 2022-06-01 11:35:43 -04:00
John Maynard
8a5e89e24b Switch Jinja2 environment for rendering before testing JSON to ImmutableSandboxedEnvironment
Render Jinja template before checking for valid JSON
2022-06-01 11:10:15 -04:00
Kersom
8c3e289170 Merge pull request #12178 from Tioborto/feat/add-token-description-column
feat: add token description column
2022-06-01 10:17:28 -04:00
Seth Foster
9364c8e562 typo 2022-05-31 17:18:45 -04:00
Seth Foster
5831949ebf maxconn 2022-05-31 17:16:27 -04:00
Seth Foster
7fe98a670f haproxy 2022-05-31 17:12:19 -04:00
Seth Foster
6f68f3cba6 Add make prometheus and make grafana commands to dev environment 2022-05-31 17:07:15 -04:00
Alex Corey
4dc956c76f Merge pull request #12275 from ansible/dependabot/npm_and_yarn/awx/ui/devel/ace-builds-1.5.1
Bump ace-builds from 1.4.12 to 1.5.1 in /awx/ui
2022-05-31 10:32:25 -04:00
Alex Corey
11a56117eb Merge pull request #12284 from ansible/dependabot/npm_and_yarn/awx/ui/devel/codemirror-5.65.4
Bump codemirror from 5.61.0 to 5.65.4 in /awx/ui
2022-05-31 10:31:51 -04:00
Alex Corey
10eed6286a Merge pull request #12285 from ansible/dependabot/npm_and_yarn/awx/ui/devel/styled-components-5.3.5
Bump styled-components from 5.3.0 to 5.3.5 in /awx/ui
2022-05-31 10:31:09 -04:00
Jessica Steurer
d36befd9ce Merge pull request #12283 from jainnikhil30/add_forks_to_job_details
add forks to the job details
2022-05-26 18:03:29 -03:00
Jessica Steurer
0c4ddc7f6f Merge pull request #12280 from nixocio/ui_issue_12279
Allow to copy entity within the minute
2022-05-26 14:09:35 -03:00
nixocio
3ef9679de3 Allow to copy entity within the minute
Allow to copy entity within the minute - add seconds, and miliseconds as part of the name
of copied entity.

See: https://github.com/ansible/awx/issues/12279
2022-05-25 16:35:22 -04:00
dependabot[bot]
d36441489a Bump styled-components from 5.3.0 to 5.3.5 in /awx/ui
Bumps [styled-components](https://github.com/styled-components/styled-components) from 5.3.0 to 5.3.5.
- [Release notes](https://github.com/styled-components/styled-components/releases)
- [Changelog](https://github.com/styled-components/styled-components/blob/main/CHANGELOG.md)
- [Commits](https://github.com/styled-components/styled-components/compare/v5.3.0...v5.3.5)

---
updated-dependencies:
- dependency-name: styled-components
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-25 18:46:16 +00:00
Alex Corey
d26c12dd7c Merge pull request #12243 from ansible/dependabot/npm_and_yarn/awx/ui/devel/patternfly/patternfly-4.194.4
Bump @patternfly/patternfly from 4.192.1 to 4.194.4 in /awx/ui
2022-05-25 14:44:27 -04:00
dependabot[bot]
7fa7ed3658 Bump @patternfly/patternfly from 4.192.1 to 4.194.4 in /awx/ui
Bumps [@patternfly/patternfly](https://github.com/patternfly/patternfly) from 4.192.1 to 4.194.4.
- [Release notes](https://github.com/patternfly/patternfly/releases)
- [Changelog](https://github.com/patternfly/patternfly/blob/main/RELEASE-NOTES.md)
- [Commits](https://github.com/patternfly/patternfly/compare/prerelease-v4.192.1...prerelease-v4.194.4)

---
updated-dependencies:
- dependency-name: "@patternfly/patternfly"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-25 18:16:48 +00:00
Jessica Steurer
2c68e7a3d2 Merge pull request #12247 from nixocio/ui_issue_12129
Add job_explanation job details page
2022-05-25 14:45:39 -03:00
Alex Corey
0c9b1c3c79 Merge pull request #12274 from ansible/dependabot/npm_and_yarn/awx/ui/devel/lingui/react-3.13.3
Bump @lingui/react from 3.9.0 to 3.13.3 in /awx/ui
2022-05-25 12:09:01 -04:00
dependabot[bot]
e10b0e513e Bump @lingui/react from 3.9.0 to 3.13.3 in /awx/ui
Bumps [@lingui/react](https://github.com/lingui/js-lingui) from 3.9.0 to 3.13.3.
- [Release notes](https://github.com/lingui/js-lingui/releases)
- [Changelog](https://github.com/lingui/js-lingui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lingui/js-lingui/compare/v3.9.0...v3.13.3)

---
updated-dependencies:
- dependency-name: "@lingui/react"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-25 15:49:56 +00:00
dependabot[bot]
68c66edada Bump ace-builds from 1.4.12 to 1.5.1 in /awx/ui
Bumps [ace-builds](https://github.com/ajaxorg/ace-builds) from 1.4.12 to 1.5.1.
- [Release notes](https://github.com/ajaxorg/ace-builds/releases)
- [Changelog](https://github.com/ajaxorg/ace-builds/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ajaxorg/ace-builds/compare/v1.4.12...v1.5.1)

---
updated-dependencies:
- dependency-name: ace-builds
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-25 15:49:30 +00:00
dependabot[bot]
6eb17e7af7 Bump codemirror from 5.61.0 to 5.65.4 in /awx/ui
Bumps [codemirror](https://github.com/codemirror/CodeMirror) from 5.61.0 to 5.65.4.
- [Release notes](https://github.com/codemirror/CodeMirror/releases)
- [Changelog](https://github.com/codemirror/CodeMirror/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codemirror/CodeMirror/compare/5.61.0...5.65.4)

---
updated-dependencies:
- dependency-name: codemirror
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-25 15:49:11 +00:00
Alex Corey
9a24da3098 Merge pull request #12281 from ansible/dependabot/npm_and_yarn/awx/ui/devel/patternfly/react-table-4.83.1
Bump @patternfly/react-table from 4.75.2 to 4.83.1 in /awx/ui
2022-05-25 11:48:18 -04:00
Nikhil
8ed0543b8b add forks to the job details 2022-05-25 20:07:38 +05:30
dependabot[bot]
73a84444d1 Bump @patternfly/react-table from 4.75.2 to 4.83.1 in /awx/ui
Bumps [@patternfly/react-table](https://github.com/patternfly/patternfly-react) from 4.75.2 to 4.83.1.
- [Release notes](https://github.com/patternfly/patternfly-react/releases)
- [Commits](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-table@4.75.2...@patternfly/react-table@4.83.1)

---
updated-dependencies:
- dependency-name: "@patternfly/react-table"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-24 19:40:54 +00:00
Alex Corey
451767c179 Merge pull request #12207 from ansible/dependabot/npm_and_yarn/awx/ui/devel/eslint-plugin-i18next-5.2.1
Bump eslint-plugin-i18next from 5.1.2 to 5.2.1 in /awx/ui
2022-05-24 15:39:40 -04:00
Alan Rominger
8366386126 Merge pull request #12260 from AlanCoding/callback_status
Fix the callback receiver --status command
2022-05-24 15:26:02 -04:00
Alex Corey
997686a2ea Merge pull request #12257 from AlexSCorey/updateDependabot
Dependabot runs monthly and only makes prs for production dependencies
2022-05-24 09:35:09 -04:00
HampusLundqvist
f02212b1fe return event_status on all gitlab webhook types 2022-05-23 22:13:00 +02:00
Jessica Steurer
2ba68ef5d0 Merge pull request #12249 from keithjgrant/filter-ws-jobs
use qs params when fetching new/updated jobs to preserve filters
2022-05-19 17:24:52 -03:00
djyasin
2041665880 Merge pull request #12227 from ansible/vaultcredentialsbug
Prevent edit of  vault ID once credential is created.
2022-05-19 15:13:41 -04:00
Alan Rominger
1e6ca01686 Fix the callback receiver --status command 2022-05-19 15:00:49 -04:00
Alex Corey
e15a76e7aa Dependabot runs monthly and only makes prs for production dependencies 2022-05-19 11:16:51 -04:00
Alex Corey
64db44acef Adds popover for Notification Templates and Instance group details (#12197) 2022-05-18 19:04:21 -04:00
Keith J. Grant
9972389a8d fetch relevant jobs based on WS events 2022-05-18 14:40:18 -07:00
Seth Foster
e0b1274eee Merge pull request #12094 from sean-m-sullivan/wait
update awx collection wait interval to 2
2022-05-18 15:00:24 -04:00
Jeff Bradberry
973facebba Double escape all unicode escape sequences in job events data
when collecting it for analytics.
2022-05-18 12:00:03 -04:00
sean-m-sullivan
df649e2c56 update awx collection wait interval to 2 2022-05-18 09:57:40 -04:00
nixocio
a778017efb Add job_explanation job details page
Add job_explanation job details page

See: https://github.com/ansible/awx/issues/12129
2022-05-18 09:16:39 -04:00
Keith J. Grant
6a9305818e use qs params when fetching new/updated jobs to preserve filters 2022-05-17 14:57:57 -07:00
Alexandre Bortoluzzi
2669904c72 fix: header row style 2022-05-17 23:04:34 +02:00
Kersom
35529b5eeb Add help text popovers to /#/applications details fields (#12222)
Add help text popovers to /#/applications details fields

See: https://github.com/ansible/awx/issues/11873
2022-05-17 20:11:51 +00:00
Sarah Akus
d55ed8713c Merge pull request #12239 from kialam/fix-12228-edit-deleted-wf-node
Fix on save error message for wf approval nodes.
2022-05-17 12:07:45 -04:00
Kersom
7973f28bed Merge pull request #12237 from ansible/dependabot/npm_and_yarn/awx/ui/devel/mock-socket-9.1.3
Bump mock-socket from 9.0.3 to 9.1.3 in /awx/ui
2022-05-17 11:31:45 -04:00
dependabot[bot]
8189964cce Bump mock-socket from 9.0.3 to 9.1.3 in /awx/ui
Bumps [mock-socket](https://github.com/thoov/mock-socket) from 9.0.3 to 9.1.3.
- [Release notes](https://github.com/thoov/mock-socket/releases)
- [Changelog](https://github.com/thoov/mock-socket/blob/master/CHANGELOG.md)
- [Commits](https://github.com/thoov/mock-socket/commits)

---
updated-dependencies:
- dependency-name: mock-socket
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-17 14:24:27 +00:00
Sarah Akus
ee4c901dc7 Merge pull request #12210 from ansible/dependabot/npm_and_yarn/awx/ui/devel/react-error-boundary-3.1.4
Bump react-error-boundary from 3.1.3 to 3.1.4 in /awx/ui
2022-05-17 10:17:44 -04:00
Lila
78220cad82 Disables ability to edit vault ID on the UI side. 2022-05-16 16:56:29 -04:00
Lila
40279bc6c0 Wrote corresponding tests.
Updated verbiage to be more in line with existing messages.
2022-05-16 16:55:49 -04:00
Lila
f6fb46d99e Prevent edit of vault ID once credential is created and added check to ensure user is actually trying to change vault id. 2022-05-16 16:54:03 -04:00
Kia Lam
954b32941e Fix on save error message for wf approval nodes. 2022-05-16 11:17:23 -07:00
Seth Foster
48b016802c Merge pull request #12049 from fosterseth/awxkit_import_help_text
Improve awxkit import -h
2022-05-16 11:59:44 -04:00
Alex Corey
35aa5dd79f Merge pull request #12212 from ansible/dependabot/npm_and_yarn/awx/ui/devel/luxon-2.4.0
Bump luxon from 2.0.1 to 2.4.0 in /awx/ui
2022-05-16 09:44:09 -04:00
JST
237402068c Merge pull request #12073 from fosterseth/scm_invsrc_project_update
SCM inv source should trigger project update
2022-05-16 09:17:45 -03:00
Kersom
31dda6e9d6 Add help text popovers to /#/execution_environments details fields (#12224)
Add help text popovers to /#/execution_environments details fields

See: https://github.com/ansible/awx/issues/11874
2022-05-13 14:53:36 -04:00
Alan Rominger
bca6e00e37 Change Demo Project status to successful 2022-05-12 16:14:09 -04:00
Sarah Akus
1c9b4af61d Merge pull request #12213 from nixocio/ui_issue_5727
Add details related workflow job on the workflow approval details
2022-05-12 16:02:25 -04:00
Seth Foster
eba4a3f1c2 in case we fail a job in task manager, we need to add the project update to the inventoryupdate.source_project field 2022-05-12 15:21:17 -04:00
Seth Foster
0ae9fe3624 if dependency fails, fail job in task manager 2022-05-12 14:00:13 -04:00
Seth Foster
1b662fcca5 SCM inv source trigger project update
- scm based inventory sources should launch project updates prior to
running inventory updates for that source.
- fixes scenario where a job is based on projectA, but the inventory
source is based on projectB. Running the job will likely trigger a
sync for projectA, but not projectB.

comments
2022-05-12 14:00:12 -04:00
John Westcott IV
cfdba959dd Falling back to project.status if the last project sync job was deleted (#12215) 2022-05-12 12:22:04 -04:00
John Westcott IV
78660ad0a2 Updated dependencies to reduce issues with dependabot and container scanning (#12180)
Modify updater.sh to remove the local path references.
2022-05-12 09:25:36 -04:00
kialam
70697869d7 Merge pull request #12220 from kialam/add-popover-detail-job-templates
Fix pop over text for job template details page.
2022-05-11 18:34:42 -07:00
Kia Lam
10e55108ef Fix pop over text for job template details page. 2022-05-11 16:14:58 -07:00
JST
d4223b8877 Merge pull request #12204 from kialam/add-popover-detail-job-templates
Add popover text to JT and WJT details pages.
2022-05-11 17:39:47 -03:00
Shane McDonald
9537d148d7 Merge pull request #12175 from TheRealHaoLiu/change-ee-container-volume-selinux-label
change SELinux label for EE volume mount
2022-05-11 16:00:02 -04:00
Kia Lam
a133a14b70 Fix unit tests. 2022-05-11 12:29:32 -07:00
Jeff Bradberry
4ca9e9577b Merge pull request #12216 from jangel97/devel
add param all_pages to method export_assets
2022-05-11 14:51:50 -04:00
Jose Angel Morena
44986fad36 set all_pages to True by default in get_method 2022-05-11 19:54:26 +02:00
Jose Angel Morena
eb2fca86b6 set all_pages to True by default in get_method 2022-05-11 19:52:32 +02:00
nixocio
458a1fc035 Add details related workflow job on the workflow approval details
Add details related workflow job on the work flow approval details

Remove not used prop isLoading, fix, and expand unit-tests related to
workflow approval details.
2022-05-11 13:32:59 -04:00
Kia Lam
6e87b29e92 Add help text to JT and WJT forms. 2022-05-11 09:10:22 -07:00
Kia Lam
be1d0c525c Add popover text to JT and WJT details pages. 2022-05-11 09:10:21 -07:00
Alex Corey
0787cb4fc2 Merge pull request #12185 from AlexSCorey/8690-SortSchedulesByType
Adds sorting by type on the schedules list
2022-05-11 10:57:10 -04:00
dependabot[bot]
19063a2d90 Bump luxon from 2.0.1 to 2.4.0 in /awx/ui
Bumps [luxon](https://github.com/moment/luxon) from 2.0.1 to 2.4.0.
- [Release notes](https://github.com/moment/luxon/releases)
- [Changelog](https://github.com/moment/luxon/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moment/luxon/compare/2.0.1...2.4.0)

---
updated-dependencies:
- dependency-name: luxon
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-11 14:40:47 +00:00
Alex Corey
e8e2f820d2 Merge pull request #12153 from ansible/dependabot/npm_and_yarn/awx/ui/devel/d3-7.4.4
Bump d3 from 7.1.1 to 7.4.4 in /awx/ui
2022-05-11 10:35:29 -04:00
Alan Rominger
aaad634483 Only use in-memory cache for database settings, set ttl=5 (#12166)
* Only use in-memory cache for database settings

Make necessary adjustments to monkeypatch
  as it is very vunerable to recursion
  Remove migration exception that is now redundant

Clear cache if a setting is changed

* Use dedicated middleware for setting cache stuff
  Clear cache for each request

* Add tests for in-memory cache
2022-05-10 21:58:22 -04:00
dependabot[bot]
dfa4127bae Bump react-error-boundary from 3.1.3 to 3.1.4 in /awx/ui
Bumps [react-error-boundary](https://github.com/bvaughn/react-error-boundary) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/bvaughn/react-error-boundary/releases)
- [Changelog](https://github.com/bvaughn/react-error-boundary/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bvaughn/react-error-boundary/compare/v3.1.3...v3.1.4)

---
updated-dependencies:
- dependency-name: react-error-boundary
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-10 21:14:25 +00:00
Jeff Bradberry
f3725c714a Merge pull request #12119 from kimbernator/devel
Remove hardcoded public schema in cleanup_jobs.py
2022-05-10 17:14:11 -04:00
dependabot[bot]
cef3ed01ac Bump eslint-plugin-i18next from 5.1.2 to 5.2.1 in /awx/ui
Bumps [eslint-plugin-i18next](https://github.com/edvardchen/eslint-plugin-i18next) from 5.1.2 to 5.2.1.
- [Release notes](https://github.com/edvardchen/eslint-plugin-i18next/releases)
- [Changelog](https://github.com/edvardchen/eslint-plugin-i18next/blob/main/CHANGELOG.md)
- [Commits](https://github.com/edvardchen/eslint-plugin-i18next/compare/v5.1.2...v5.2.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-i18next
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-10 21:12:30 +00:00
Alex Corey
fc1a3f46f9 Merge pull request #12154 from ansible/dependabot/npm_and_yarn/awx/ui/devel/patternfly/react-table-4.75.2
Bump @patternfly/react-table from 4.67.19 to 4.75.2 in /awx/ui
2022-05-10 17:10:16 -04:00
Sarabraj Singh
bfa5feb51b Merge pull request #12205 from sarabrajsingh/revert-and-fix-12186
Revert and fix 12186
2022-05-10 15:29:56 -04:00
Sarabraj Singh
4c0813bd69 deleting folder contents using find command 2022-05-10 14:43:27 -04:00
Sarabraj Singh
9b0b0f2a5f Revert "fixing rm -Rf logic to delete contents of folder but leave parent folder intact"
This reverts commit df2d303ab0.
2022-05-10 14:06:09 -04:00
JST
e87c121f8f Merge pull request #12156 from mabashian/large-workflow-crash
Don't repeatedly traverse workflow nodes when finding ancestors
2022-05-10 14:41:49 -03:00
Seth Foster
65dfc424bc Improve help text for import and export 2022-05-10 13:18:40 -04:00
Sarabraj Singh
dfea9cc526 Merge pull request #12186 from sarabrajsingh/bugfix-delete-on-update-11733
fixing rm -Rf logic to delete contents of folder
2022-05-09 16:08:39 -04:00
Rebeccah Hunter
0d97a0364a Merge pull request #12170 from ansible/give_us_the_deets
Update triage_replies give us more info
2022-05-09 15:22:57 -04:00
kialam
1da57a4a12 Merge pull request #12191 from kialam/fix-12188-undefined-wf-approval-list
Fix deleted wf approval node name.
2022-05-09 11:16:21 -07:00
Rebeccah Hunter
b73078e9db Merge pull request #11373 from rebeccahhh/fix-settings_cache_threading_awx
add lock to cachetools usage
2022-05-09 13:56:16 -04:00
Kia Lam
b17f22cd38 Fix unit tests. 2022-05-09 10:55:51 -07:00
Alex Corey
7b225057ce Merge pull request #12198 from AlexSCorey/fixPRLabeler
Prevents the api label from being added to UI only PRs
2022-05-09 13:25:51 -04:00
Alex Corey
8242078c06 Prevents the api label from being added to UI only PRs 2022-05-09 11:17:22 -04:00
John Westcott IV
a86740c3c9 Adding ability to start and plumb splunk instance (#12183) 2022-05-09 09:50:28 -04:00
Kia Lam
cbde56549d Fix deleted wf approval node name. 2022-05-06 13:51:16 -07:00
CWollinger
385a94866c add tooltip for checkbox in DataListToolbar (#12133)
Signed-off-by: CWollinger <CWollinger@web.de>
2022-05-06 16:36:07 -04:00
chris meyers
21972c91dd add lock to cachetools usage
* We observed daphne giving tracebacks when accessing logging settings.
  Originally, configure tower in tower settings was no a suspect because
  daphne is not multi-process. We've had issues with configure tower in
  tower settings and multi-process before. We later learned that Daphne
  is multi-threaded. Configure tower in tower was back to being a
  suspect. We constructed a minimal reproducer to show that multiple
  threads accessing settings can cause the same traceback that we saw in
  daphne. See
  https://gist.github.com/chrismeyersfsu/7aa4bdcf76e435efd617cb078c64d413
  for that recreator. These fixes stop the recreation.
2022-05-06 16:24:36 -04:00
JST
36d3f9afdb Merge pull request #12184 from marshmalien/2912-prefill-playbook
Autopopulate playbook field when there is one resource
2022-05-06 17:18:18 -03:00
Sarabraj Singh
df2d303ab0 fixing rm -Rf logic to delete contents of folder but leave parent folder intact 2022-05-06 15:41:34 -04:00
Alex Corey
05eba350b7 Adds sorting by type on the schedules list. Also adds functionality for bulk_data command to create schedules 2022-05-06 09:45:45 -04:00
Alexandre Bortoluzzi
1e12e12578 style: prettier file 2022-05-06 14:32:54 +02:00
Alexandre Bortoluzzi
bbdab82433 fix: user token list item tests 2022-05-06 14:26:10 +02:00
kialam
f7be6b6423 Remove timezone formatting for date picker entry. (#12163) 2022-05-05 16:46:38 -04:00
Marliana Lara
ba358eaa4f Autopopulate playbook field when there is one resource 2022-05-05 16:12:26 -04:00
JST
162e09972f Merge pull request #12172 from keithjgrant/11869-users-help-text
Add help text to user token detail
2022-05-05 15:35:14 -03:00
JST
2cfccdbe16 Merge pull request #12158 from nixocio/ui_issue_11862
Add help text popovers to /#/credentials details fields
2022-05-05 14:00:15 -03:00
Kersom
434fa7b7be Merge pull request #12161 from nixocio/ui_css_details
Adding popover for details is showing breaking of words
2022-05-05 16:34:49 +03:00
Sarah Akus
2f8bdf1eab Merge pull request #12173 from kialam/fix-12167-unresponsive-datepicker
Upgrade @patternfly/react-core.
2022-05-05 09:16:13 -04:00
Alexandre Bortoluzzi
e1705738a1 fix: french vocabulary 2022-05-05 12:51:35 +02:00
Alexandre Bortoluzzi
4cfb8fe482 feat: display token description on user tokens list page 2022-05-05 12:51:17 +02:00
Hao Liu
d52d2af4b4 change SELinux label for EE volume mount
- The `z` option indicates that the bind mount content is shared among multiple containers.
- The `Z` option indicates that the bind mount content is private and unshared.

If multiple container attempt to mount the same directory `Z` option will cause a raise condition where only the last container started will have access to the file.

Ref: https://docs.docker.com/storage/bind-mounts/#configure-the-selinux-label
Signed-off-by: Hao Liu <haoli@redhat.com>
2022-05-04 21:31:34 -04:00
Kia Lam
97fd3832d4 Upgrade @patternfly/react-core. 2022-05-04 14:19:21 -07:00
Keith J. Grant
3cedd0e0bd add help text to user token detail 2022-05-04 13:23:28 -07:00
Rebeccah Hunter
507b1898ce Update triage_replies.md 2022-05-04 15:28:28 -04:00
Kersom
e3fe9010b7 Merge pull request #12152 from ansible/dependabot/npm_and_yarn/awx/ui/devel/testing-library/react-12.1.5
Bump @testing-library/react from 12.1.4 to 12.1.5 in /awx/ui
2022-05-04 20:58:20 +03:00
Kersom
2c350b8b90 Merge pull request #12151 from ansible/dependabot/npm_and_yarn/awx/ui/devel/patternfly/patternfly-4.192.1
Bump @patternfly/patternfly from 4.183.1 to 4.192.1 in /awx/ui
2022-05-04 11:04:25 -04:00
nixocio
d74e258079 Add help text popovers to /#/credentials details fields
Add help text popovers to /#/credentials details fields

See: https://github.com/ansible/awx/issues/11862
2022-05-04 09:29:39 -04:00
nixocio
b03cabd314 Adding popover for details is showing breaking of words
Now that we are adding popovers for details pages, I noticed a couple of
strings wrapping in odd places, update css to avoid that.

Also `word-break: break-word` was deprecated.
2022-05-03 16:54:32 -04:00
Keith Grant
6a63af83c0 Merge pull request #12150 from keithjgrant/add-old-version-message-to-triage-replies
add old version message to triage replies
2022-05-03 11:47:07 -07:00
Alan Rominger
452744b67e Delay update of artifacts and error fields until final job save (#11832)
* Delay update of artifacts until final job save

Save tracebacks from receptor module to callback object

Move receptor traceback check up to be more logical

Use new mock_me fixture to avoid DB call with me method

Update the special runner message to the delay_update pattern

* Move special runner message into post-processing of callback fields
2022-05-03 14:42:50 -04:00
mabashian
703a68d4fe Don't repeatedly traverse workflow nodes when finding ancestors 2022-05-03 13:39:03 -04:00
dependabot[bot]
557893e4b0 Bump @patternfly/react-table from 4.67.19 to 4.75.2 in /awx/ui
Bumps [@patternfly/react-table](https://github.com/patternfly/patternfly-react) from 4.67.19 to 4.75.2.
- [Release notes](https://github.com/patternfly/patternfly-react/releases)
- [Commits](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-table@4.67.19...@patternfly/react-table@4.75.2)

---
updated-dependencies:
- dependency-name: "@patternfly/react-table"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-03 16:39:23 +00:00
dependabot[bot]
d7051fb6ce Bump d3 from 7.1.1 to 7.4.4 in /awx/ui
Bumps [d3](https://github.com/d3/d3) from 7.1.1 to 7.4.4.
- [Release notes](https://github.com/d3/d3/releases)
- [Changelog](https://github.com/d3/d3/blob/main/CHANGES.md)
- [Commits](https://github.com/d3/d3/compare/v7.1.1...v7.4.4)

---
updated-dependencies:
- dependency-name: d3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-03 16:38:37 +00:00
dependabot[bot]
867c50da19 Bump @testing-library/react from 12.1.4 to 12.1.5 in /awx/ui
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 12.1.4 to 12.1.5.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v12.1.4...v12.1.5)

---
updated-dependencies:
- dependency-name: "@testing-library/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-03 16:38:14 +00:00
dependabot[bot]
e8d76ec272 Bump @patternfly/patternfly from 4.183.1 to 4.192.1 in /awx/ui
Bumps [@patternfly/patternfly](https://github.com/patternfly/patternfly) from 4.183.1 to 4.192.1.
- [Release notes](https://github.com/patternfly/patternfly/releases)
- [Changelog](https://github.com/patternfly/patternfly/blob/main/RELEASE-NOTES.md)
- [Commits](https://github.com/patternfly/patternfly/compare/prerelease-v4.183.1...prerelease-v4.192.1)

---
updated-dependencies:
- dependency-name: "@patternfly/patternfly"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-03 16:37:37 +00:00
Keith J. Grant
c102c61532 add old version message to triage replies 2022-05-03 08:20:45 -07:00
John Westcott IV
adb2b0da89 Adding standard message for AWX release (#12105) 2022-05-03 09:24:48 -04:00
JST
3610008699 Merge pull request #12145 from marshmalien/4057-project-scm-type-details
Add SCM Type detail to job detail view
2022-05-03 10:09:35 -03:00
kialam
3b44838dde Merge pull request #12146 from nixocio/ui_all
Update dependabot
2022-05-02 14:46:58 -07:00
nixocio
0205d7deab Update dependabot
dependency-type to be direct
2022-05-02 17:15:54 -04:00
Marliana Lara
dd47829bdb Add SCM Type detail to job details 2022-05-02 16:25:46 -04:00
JST
e7e72d13a9 Merge pull request #12137 from keithjgrant/10831-inventory-source-project-validation
remove incorrect form error message in inv source
2022-05-02 17:00:15 -03:00
kialam
4bbdf1ec8a Merge pull request #12138 from kialam/add-directory-dependabot
Add directory destination to dependabot yaml file
2022-05-02 12:30:53 -07:00
Kia Lam
4596df449e Add directory. 2022-05-02 12:07:50 -07:00
Christian M. Adams
2b0846e8a2 Bump Receptorctl to 1.2.3 2022-05-02 14:41:04 -04:00
Kersom
ecbb636ba1 Merge pull request #12136 from nixocio/ui_fix_css_schedules
Align items on schedule form
2022-05-02 14:29:57 -04:00
Keith J. Grant
e3aed9dad4 remove incorrect form error message in inv source 2022-05-02 10:34:33 -07:00
JST
213983a322 Merge pull request #12078 from AlexSCorey/12058-CleanUpReactWarnings
Cleans up some console warnings.
2022-05-02 13:13:20 -03:00
nixocio
2977084787 Align items on schedule form
Align items on schedule form
2022-05-02 11:11:34 -04:00
Sarah Akus
b6362a63cc Merge pull request #12134 from ansible/add-deleted-inventory-locator-for-JT-detail
add new locator for deleted inventory in JT detail screen
2022-04-29 16:18:45 -04:00
akus062381
7517ba820b add new locator for deleted inventory 2022-04-29 15:57:32 -04:00
Alan Rominger
29d60844a8 Fix notification timing issue by sending in the latter of 2 events (#12110)
* Track host_status_counts and use that to process notifications

* Remove now unused setting

* Back out changes to callback class not needed after all

* Skirt the need for duck typing by leaning on the cached field

* Delete tests for deleted task

* Revert "Back out changes to callback class not needed after all"

This reverts commit 3b8ae350d218991d42bffd65ce4baac6f41926b2.

* Directly hardcode stats_event_type for callback class

* Fire notifications if stats event was never sent

* Remove test content for deleted methods

* Add placeholder for when no hosts matched

* Make field default be None, denote events processed with empty dict

* Make UI process null value for host_status_counts

* Fix tracking of EOF dispatch for system jobs

* Reorganize EVENT_MAP into class properties

* Consolidate conditional I missed from EVENT_MAP refactor

* Give up on the null condition, also applies for empty hosts

* Remove cls position argument not being used

* Move wrapup method out of class, add tests
2022-04-29 13:54:31 -04:00
Alex Corey
41b0607d7e Merge pull request #12108 from marshmalien/org-host-credTypes-helpText
Add organization, host, and credential type detail view help text
2022-04-29 13:13:41 -04:00
John Westcott IV
13f7166a30 Fixing write location of ssh_key-data-cert.pub (#12122) 2022-04-29 12:22:09 -04:00
Sarah Akus
0cc9b84ead Merge pull request #11998 from mabashian/344-host-count
Adds total host count to inv and smart inv details views
2022-04-29 10:37:26 -04:00
JST
68ee4311bf Merge pull request #12128 from mabashian/11990-schedule-prompt-tags-v2
Add tags/skip tags to the list of things that will cause the Prompt button to be displayed on the schedule form
2022-04-28 17:23:13 -03:00
Alex Corey
6e6c3f676e Merge pull request #12120 from AlexSCorey/addDependabot
Add dependabot for ui
2022-04-28 15:43:03 -04:00
John Westcott IV
c67f50831b Modifying schedules API to allow for rrulesets #5733 (#12043)
* Added schedule_rruleset lookup plugin for awx.awx
* Added DB migration for rrule size
* Updated schedule docs
* The schedule API endpoint will now return an array of errors on rule validation to try and inform the user of all errors instead of just the first
2022-04-28 15:38:20 -04:00
Alex Corey
50ef234bd6 Update .github/dependabot.yml
Co-authored-by: Marliana Lara <marliana.lara@gmail.com>
2022-04-28 15:14:08 -04:00
Jeff Bradberry
2bef5ce09b Merge pull request #12099 from jbradberry/add-content-type-option-header
Add the X-Content-Type-Options nosniff header
2022-04-28 14:41:02 -04:00
Seth Foster
a49c4796f4 Merge pull request #12115 from sean-m-sullivan/workflow_node_updates
update workflow nodes to allow workflows and system jobs
2022-04-28 14:12:33 -04:00
Seth Foster
9eab9586e5 Merge pull request #12114 from sean-m-sullivan/awx_collection_alias
update awx collection workflow module
2022-04-28 13:42:30 -04:00
mabashian
cd35787a86 Adds total host count to inv and smart inv details views 2022-04-28 11:40:27 -04:00
mabashian
cbe84ff4f3 Add tags/skip tags to the list of things that will cause the Prompt button to be displayed on the schedule form 2022-04-28 11:33:46 -04:00
Alex Corey
410f38eccf add dependabot for ui 2022-04-28 09:30:54 -04:00
Sarah Akus
b885fc2d86 Merge pull request #12123 from marshmalien/12109-fix-user-role-association
Fix user role association in access modal
2022-04-27 19:23:53 -04:00
JST
4c93f5794a Merge pull request #12098 from nixocio/ui_work_flow
Fix broken job WFJT details when related JT is deleted
2022-04-27 17:47:28 -03:00
sean-m-sullivan
456bb75dcb update awx collection workflow module 2022-04-27 16:32:37 -04:00
sean-m-sullivan
02fd8b0d20 update workflow nodes 2022-04-27 16:18:00 -04:00
Marliana Lara
fbe6c80f86 Fix user role association in access modal 2022-04-27 16:01:15 -04:00
Jeremy Kimber
3d5f302d10 remove hardcoded public schema in cleanup_jobs.py 2022-04-27 12:45:15 -05:00
Sarah Akus
856a2c1734 Merge pull request #12107 from keithjgrant/12101-job-output-single-item-pagination
fix off-by-one error in job output pagination
2022-04-27 10:43:20 -04:00
John Westcott IV
4277b73438 Adding /etc/supervisord.conf to sosreports (#12104) 2022-04-27 10:34:45 -04:00
Alex Corey
2888f9f8d0 Cleans up some console warnings. 2022-04-26 17:17:41 -04:00
Jeff Bradberry
68221cdcbe Merge pull request #12106 from jbradberry/django-bump
Bump Django to 3.2.13
2022-04-26 15:07:52 -04:00
Sean Sullivan
f50501cc2a update awx.awx collection to allow remote project. (#12093) 2022-04-26 15:07:29 -04:00
Marliana Lara
c84fac65e0 Add organization, host, and credential type detail view help text. 2022-04-26 11:50:36 -04:00
Jeff Bradberry
d64c457b3d Bump Django to 3.2.13 2022-04-26 10:34:28 -04:00
Keith J. Grant
1bd5a880dc fix off-by-one error in job output pagination 2022-04-25 13:09:44 -07:00
Jeff Bradberry
47d5a89f40 Add the X-Content-Type-Options nosniff header 2022-04-25 13:45:16 -04:00
nixocio
6060e7e29f Fix broken job WFJT details when related JT is deleted
Fix broken job WFJT details when related JT is deleted
2022-04-25 12:33:12 -04:00
Alan Rominger
29702400f1 Avoid parent instance update when status was unchanged 2022-04-22 09:07:03 -04:00
Jeff Bradberry
b562d5cc88 Look up the correct top-level resource name when reconstructing foreign keys
during an awx-cli export.
2022-03-18 10:32:33 -04:00
Andrew Kelling
dfde30798e Update README.md
Cleaned up wording
2021-12-07 11:59:11 -07:00
640 changed files with 42667 additions and 26355 deletions

View File

@@ -1,3 +1,2 @@
awx/ui/node_modules
Dockerfile
.git

17
.github/BOTMETA.yml vendored
View File

@@ -1,17 +0,0 @@
---
files:
awx/ui/:
labels: component:ui
maintainers: $team_ui
awx/api/:
labels: component:api
maintainers: $team_api
awx/main/:
labels: component:api
maintainers: $team_api
installer/:
labels: component:installer
macros:
team_api: wwitzel3 matburt chrismeyersfsu cchurch AlanCoding ryanpetrello rooftopcellist
team_ui: jlmitch5 jaredevantabor mabashian marshmalien benthomasson jakemcdermott

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
workflows/e2e_test.yml @tiagodread @shanemcd @jakemcdermott

View File

@@ -6,17 +6,37 @@ practices regarding responsible disclosure, see
https://www.ansible.com/security
-->
<!--
PLEASE DO NOT USE A BLANK TEMPLATE IN THE AWX REPO.
This is a legacy template used for internal testing ONLY.
Any issues opened will this template will be automatically closed.
Instead use the bug or feature request.
-->
##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
- Bug Report
- Feature Idea
- Documentation
- Breaking Change
- New or Enhanced Feature
- Bug, Docs Fix or other nominal change
##### COMPONENT NAME
<!-- Pick the area of AWX for this issue, you can have multiple, delete the rest: -->
- API
- UI
- Collection
- Docs
- CLI
- Other
##### SUMMARY
<!-- Briefly describe the problem. -->

View File

@@ -1,13 +1,12 @@
---
name: Bug Report
description: Create a report to help us improve
description: "🐞 Create a report to help us improve"
body:
- type: markdown
attributes:
value: |
Issues are for **concrete, actionable bugs and feature requests** only. For debugging help or technical support, please use:
- The #ansible-awx channel on irc.libera.chat
- The awx project mailing list, https://groups.google.com/forum/#!forum/awx-project
Bug Report issues are for **concrete, actionable bugs** only.
For debugging help or technical support, please see the [Get Involved section of our README](https://github.com/ansible/awx#get-involved)
- type: checkboxes
id: terms
@@ -24,7 +23,7 @@ body:
- type: textarea
id: summary
attributes:
label: Summary
label: Bug Summary
description: Briefly describe the problem.
validations:
required: false
@@ -45,6 +44,9 @@ body:
- label: UI
- label: API
- label: Docs
- label: Collection
- label: CLI
- label: Other
- type: dropdown
id: awx-install-method
@@ -57,9 +59,8 @@ body:
- minikube
- openshift
- minishift
- docker on linux
- docker for mac
- boot2docker
- docker development environment
- N/A
validations:
required: true

12
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
---
blank_issues_enabled: true
contact_links:
- name: For debugging help or technical support
url: https://github.com/ansible/awx#get-involved
about: For general debugging or technical support please see the Get Involved section of our readme.
- name: 📝 Ansible Code of Conduct
url: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html?utm_medium=github&utm_source=issue_template_chooser
about: AWX uses the Ansible Code of Conduct; ❤ Be nice to other members of the community. ☮ Behave.
- name: 💼 For Enterprise
url: https://www.ansible.com/products/engine?utm_medium=github&utm_source=issue_template_chooser
about: Red Hat offers support for the Ansible Automation Platform

View File

@@ -1,17 +0,0 @@
---
name: "✨ Feature request"
about: Suggest an idea for this project
---
<!-- Issues are for **concrete, actionable bugs and feature requests** only - if you're just asking for debugging help or technical support, please use:
- http://web.libera.chat/?channels=#ansible-awx
- https://groups.google.com/forum/#!forum/awx-project
We have to limit this because of limited volunteer time to respond to issues! -->
##### ISSUE TYPE
- Feature Idea
##### SUMMARY
<!-- Briefly describe the problem or desired enhancement. -->

View File

@@ -0,0 +1,88 @@
---
name: ✨ Feature request
description: Suggest an idea for this project
body:
- type: markdown
attributes:
value: |
Feature Request issues are for **feature requests** only.
For debugging help or technical support, please see the [Get Involved section of our README](https://github.com/ansible/awx#get-involved)
- type: checkboxes
id: terms
attributes:
label: Please confirm the following
options:
- label: I agree to follow this project's [code of conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html).
required: true
- label: I have checked the [current issues](https://github.com/ansible/awx/issues) for duplicates.
required: true
- label: I understand that AWX is open source software provided for free and that I might not receive a timely response.
required: true
- type: dropdown
id: feature-type
attributes:
label: Feature type
description: >-
What kind of feature is this?
multiple: false
options:
- "New Feature"
- "Enhancement to Existing Feature"
validations:
required: true
- type: textarea
id: summary
attributes:
label: Feature Summary
description: Briefly describe the desired enhancement.
validations:
required: true
- type: checkboxes
id: components
attributes:
label: Select the relevant components
options:
- label: UI
- label: API
- label: Docs
- label: Collection
- label: CLI
- label: Other
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: >-
Describe the necessary steps to understand the scenario of the requested enhancement.
Include all the steps that will help the developer and QE team understand what you are requesting.
validations:
required: true
- type: textarea
id: current-results
attributes:
label: Current results
description: What is currently happening on the scenario?
validations:
required: true
- type: textarea
id: sugested-results
attributes:
label: Sugested feature result
description: What is the result this new feature will bring?
validations:
required: true
- type: textarea
id: additional-information
attributes:
label: Additional information
description: Please provide any other information you think is relevant that could help us understand your feature request.
validations:
required: false

View File

@@ -1,9 +0,0 @@
---
name: "\U0001F525 Security bug report"
about: How to report security vulnerabilities
---
For all security related bugs, email security@ansible.com instead of using this issue tracker and you will receive a prompt response.
For more information on the Ansible community's practices regarding responsible disclosure, see https://www.ansible.com/security

View File

@@ -1,9 +0,0 @@
Bug Report: type:bug
Bugfix Pull Request: type:bug
Feature Request: type:enhancement
Feature Pull Request: type:enhancement
UI: component:ui
API: component:api
Installer: component:installer
Docs Pull Request: component:docs
Documentation: component:docs

View File

@@ -1,11 +1,3 @@
<!--- changelog-entry
# Fill in 'msg' below to have an entry automatically added to the next release changelog.
# Leaving 'msg' blank will not generate a changelog entry for this PR.
# Please ensure this is a simple (and readable) one-line string.
---
msg: ""
-->
##### SUMMARY
<!--- Describe the change, including rationale and design decisions -->
@@ -17,15 +9,18 @@ the change does.
##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
- Feature Pull Request
- Bugfix Pull Request
- Docs Pull Request
- Breaking Change
- New or Enhanced Feature
- Bug, Docs Fix or other nominal change
##### COMPONENT NAME
<!--- Name of the module/plugin/module/task -->
- API
- UI
- Collection
- CLI
- Docs
- Other
##### AWX VERSION
<!--- Paste verbatim output from `make VERSION` between quotes below -->

19
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/awx/ui"
schedule:
interval: "monthly"
open-pull-requests-limit: 5
allow:
- dependency-type: "production"
reviewers:
- "AlexSCorey"
- "keithjgrant"
- "kialam"
- "mabashian"
- "marshmalien"
labels:
- "component:ui"
- "dependencies"
target-branch: "devel"

View File

@@ -1,12 +1,16 @@
needs_triage:
- '.*'
"type:bug":
- "Please confirm the following"
- "Bug Summary"
"type:enhancement":
- "Feature Idea"
- "Feature Summary"
"component:ui":
- "\\[X\\] UI"
"component:api":
- "\\[X\\] API"
"component:docs":
- "\\[X\\] Docs"
"component:awx_collection":
- "\\[X\\] Collection"
"component:cli":
- "\\[X\\] awxkit"

View File

@@ -1,14 +1,19 @@
"component:api":
- any: ['awx/**/*', '!awx/ui/*']
- any: ["awx/**/*", "!awx/ui/**"]
"component:ui":
- any: ['awx/ui/**/*']
- any: ["awx/ui/**/*"]
"component:docs":
- any: ['docs/**/*']
- any: ["docs/**/*"]
"component:cli":
- any: ['awxkit/**/*']
- any: ["awxkit/**/*"]
"component:collection":
- any: ['awx_collection/**/*']
"component:awx_collection":
- any: ["awx_collection/**/*"]
"dependencies":
- any: ["awx/ui/package.json"]
- any: ["awx/requirements/*.txt"]
- any: ["awx/requirements/requirements.in"]

View File

@@ -1,31 +1,114 @@
## General
- For the roundup of all the different mailing lists available from AWX, Ansible, and beyond visit: https://docs.ansible.com/ansible/latest/community/communication.html
- For the roundup of all the different mailing lists available from AWX, Ansible, and beyond visit: https://docs.ansible.com/ansible/latest/community/communication.html
- Hello, we think your question is answered in our FAQ. Does this: https://www.ansible.com/products/awx-project/faq cover your question?
- You can find the latest documentation here: https://docs.ansible.com/automation-controller/latest/html/userguide/index.html
## Visit our mailing list
- Hello, your question seems like a good one to ask on our mailing list at https://groups.google.com/g/awx-project. You can also join #ansible-awx on https://libera.chat/ and ask your question there.
## Create an issue
- Hello, thanks for reaching out on list. We think this merits an issue on our Github, https://github.com/ansible/awx/issues. If you could open an issue up on Github it will get tagged and integrated into our planning and workflow. All future work will be tracked there.
## Create a Pull Request
- Hello, we think your idea is good, please consider contributing a PR for this, following our contributing guidelines: https://github.com/ansible/awx/blob/devel/CONTRIBUTING.md
## PRs/Issues
## Receptor
- You can find the receptor docs here: https://receptor.readthedocs.io/en/latest/
- Hello, your issue seems related to receptor, could you please open an issue in the receptor repository? https://github.com/ansible/receptor. Thanks!
### Visit our mailing list
- Hello, this appears to be less of a bug report or feature request and more of a question. Could you please ask this on our mailing list? See https://github.com/ansible/awx/#get-involved for information for ways to connect with us.
## Ansible Engine not AWX
- Hello, your question seems to be about Ansible development, not about AWX. Try asking on the Ansible-devel specific mailing list: https://groups.google.com/g/ansible-devel
### Denied Submission
- Hi! \
\
Thanks very much for your submission to AWX. It means a lot to us that you have taken time to contribute. \
\
At this time we do not want to merge this PR. Our reasons for this are: \
\
(A) INSERT ITEM HERE \
\
Please know that we are always up for discussion but this project is very active. Because of this, we're unlikely to see comments made on closed PRs, and we lock them after some time. If you or anyone else has any further questions, please let us know by using any of the communication methods listed in the page below: \
\
https://github.com/ansible/awx/#get-involved \
\
In the future, sometimes starting a discussion on the development list prior to implementing a feature can make getting things included a little easier, but it is not always necessary. \
\
Thank you once again for this and your interest in AWX!
### No Progress Issue
- Hi! \
\
Thank you very much for for this issue. It means a lot to us that you have taken time to contribute by opening this report. \
\
On this issue, there were comments added but it has been some time since then without response. At this time we are closing this issue. If you get time to address the comments we can reopen the issue if you can contact us by using any of the communication methods listed in the page below: \
\
https://github.com/ansible/awx/#get-involved \
\
Thank you once again for this and your interest in AWX!
### No Progress PR
- Hi! \
\
Thank you very much for your submission to AWX. It means a lot to us that you have taken time to contribute. \
\
On this PR, changes were requested but it has been some time since then. We think this PR has merit but without the requested changes we are unable to merge it. At this time we are closing your PR. If you get time to address the changes you are welcome to open another PR or we can reopen this PR upon request if you contact us by using any of the communication methods listed in the page below: \
\
https://github.com/ansible/awx/#get-involved \
\
Thank you once again for this and your interest in AWX!
## Common
### Give us more info
- Hello, we'd love to help, but we need a little more information about the problem you're having. Screenshots, log outputs, or any reproducers would be very helpful.
### Code of Conduct
- Hello. Please keep in mind that Ansible adheres to a Code of Conduct in its community spaces. The spirit of the code of conduct is to be kind, and this is your friendly reminder to be so. Please see the full code of conduct here if you have questions: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html
### EE Contents / Community General
- Hello. The awx-ee contains the collections and dependencies needed for supported AWX features to function. Anything beyond that (like the community.general package) will require you to build your own EE. For information on how to do that, see https://ansible-builder.readthedocs.io/en/stable/ \
\
The Ansible Community is looking at building an EE that corresponds to all of the collections inside the ansible package. That may help you if and when it happens; see https://github.com/ansible-community/community-topics/issues/31 for details.
## Mailing List Triage
### Create an issue
- Hello, thanks for reaching out on list. We think this merits an issue on our Github, https://github.com/ansible/awx/issues. If you could open an issue up on Github it will get tagged and integrated into our planning and workflow. All future work will be tracked there. Issues should include as much information as possible, including screenshots, log outputs, or any reproducers.
### Create a Pull Request
- Hello, we think your idea is good! Please consider contributing a PR for this following our contributing guidelines: https://github.com/ansible/awx/blob/devel/CONTRIBUTING.md
### Receptor
- You can find the receptor docs here: https://receptor.readthedocs.io/en/latest/
- Hello, your issue seems related to receptor. Could you please open an issue in the receptor repository? https://github.com/ansible/receptor. Thanks!
### Ansible Engine not AWX
- Hello, your question seems to be about Ansible development, not about AWX. Try asking on the Ansible-devel specific mailing list: https://groups.google.com/g/ansible-devel
- Hello, your question seems to be about using Ansible, not about AWX. https://groups.google.com/g/ansible-project is the best place to visit for user questions about Ansible. Thanks!
## Ansible Galaxy not AWX
- Hey there, that sounds like an FAQ question, did this: https://www.ansible.com/products/awx-project/faq cover your question?
### Ansible Galaxy not AWX
- Hey there. That sounds like an FAQ question. Did this: https://www.ansible.com/products/awx-project/faq cover your question?
## Contributing Guidelines
- AWX: https://github.com/ansible/awx/blob/devel/CONTRIBUTING.md
### Contributing Guidelines
- AWX: https://github.com/ansible/awx/blob/devel/CONTRIBUTING.md
- AWX-Operator: https://github.com/ansible/awx-operator/blob/devel/CONTRIBUTING.md
## Code of Conduct
- Hello. Please keep in mind that Ansible adheres to a Code of Conduct in its community spaces. The spirit of the code of conduct is to be kind, and this is your friendly reminder to be so. Please see the full code of conduct here if you have questions: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html
### Oracle AWX
We'd be happy to help if you can reproduce this with AWX since we do not have Oracle's Linux Automation Manager. If you need help with this specific version of Oracles Linux Automation Manager you will need to contact your Oracle for support.
### AWX Release
Subject: Announcing AWX Xa.Ya.za and AWX-Operator Xb.Yb.zb
- Hi all, \
\
We're happy to announce that the next release of AWX, version <b>`Xa.Ya.za`</b> is now available! \
In addition AWX Operator version <b>`Xb.Yb.zb`</b> has also been released! \
\
Please see the releases pages for more details: \
AWX: https://github.com/ansible/awx/releases/tag/Xa.Ya.za \
Operator: https://github.com/ansible/awx-operator/releases/tag/Xb.Yb.zb \
\
The AWX team.
## Try latest version
- Hello, this issue pertains to an older version of AWX. Try upgrading to the latest version and let us know if that resolves your issue.

View File

@@ -111,6 +111,15 @@ jobs:
repository: ansible/awx-operator
path: awx-operator
- name: Get python version from Makefile
working-directory: awx
run: echo py_version=`make PYTHON_VERSION` >> $GITHUB_ENV
- name: Install python ${{ env.py_version }}
uses: actions/setup-python@v2
with:
python-version: ${{ env.py_version }}
- name: Install playbook dependencies
run: |
python3 -m pip install docker

View File

@@ -19,3 +19,34 @@ jobs:
not-before: 2021-12-07T07:00:00Z
configuration-path: .github/issue_labeler.yml
enable-versioned-regex: 0
community:
runs-on: ubuntu-latest
name: Label Issue - Community
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v4
- name: Install python requests
run: pip install requests
- name: Check if user is a member of Ansible org
uses: jannekem/run-python-script-action@v1
id: check_user
with:
script: |
import requests
headers = {'Accept': 'application/vnd.github+json', 'Authorization': 'token ${{ secrets.GITHUB_TOKEN }}'}
response = requests.get('${{ fromJson(toJson(github.event.issue.user.url)) }}/orgs?per_page=100', headers=headers)
is_member = False
for org in response.json():
if org['login'] == 'ansible':
is_member = True
if is_member:
print("User is member")
else:
print("User is community")
- name: Add community label if not a member
if: contains(steps.check_user.outputs.stdout, 'community')
uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90
with:
add-labels: "community"
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -18,3 +18,34 @@ jobs:
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/pr_labeler.yml
community:
runs-on: ubuntu-latest
name: Label PR - Community
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v4
- name: Install python requests
run: pip install requests
- name: Check if user is a member of Ansible org
uses: jannekem/run-python-script-action@v1
id: check_user
with:
script: |
import requests
headers = {'Accept': 'application/vnd.github+json', 'Authorization': 'token ${{ secrets.GITHUB_TOKEN }}'}
response = requests.get('${{ fromJson(toJson(github.event.pull_request.user.url)) }}/orgs?per_page=100', headers=headers)
is_member = False
for org in response.json():
if org['login'] == 'ansible':
is_member = True
if is_member:
print("User is member")
else:
print("User is community")
- name: Add community label if not a member
if: contains(steps.check_user.outputs.stdout, 'community')
uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90
with:
add-labels: "community"
repo-token: ${{ secrets.GITHUB_TOKEN }}

45
.github/workflows/pr_body_check.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
---
name: PR Check
env:
BRANCH: ${{ github.base_ref || 'devel' }}
on:
pull_request:
types: [opened, edited, reopened, synchronize]
jobs:
pr-check:
name: Scan PR description for semantic versioning keywords
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Write PR body to a file
run: |
cat >> pr.body << __SOME_RANDOM_PR_EOF__
${{ github.event.pull_request.body }}
__SOME_RANDOM_PR_EOF__
- name: Display the received body for troubleshooting
run: cat pr.body
# We want to write these out individually just incase the options were joined on a single line
- name: Check for each of the lines
run: |
grep "Bug, Docs Fix or other nominal change" pr.body > Z
grep "New or Enhanced Feature" pr.body > Y
grep "Breaking Change" pr.body > X
exit 0
# We exit 0 and set the shell to prevent the returns from the greps from failing this step
# See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
shell: bash {0}
- name: Check for exactly one item
run: |
if [ $(cat X Y Z | wc -l) != 1 ] ; then
echo "The PR body must contain exactly one of [ 'Bug, Docs Fix or other nominal change', 'New or Enhanced Feature', 'Breaking Change' ]"
echo "We counted $(cat X Y Z | wc -l)"
echo "See the default PR body for examples"
exit 255;
else
exit 0;
fi

View File

@@ -21,7 +21,7 @@ jobs:
- name: Install dependencies
run: |
python${{ env.py_version }} -m pip install wheel twine
python${{ env.py_version }} -m pip install wheel twine setuptools-scm
- name: Set official collection namespace
run: echo collection_namespace=awx >> $GITHUB_ENV
@@ -33,7 +33,7 @@ jobs:
- name: Build collection and publish to galaxy
run: |
COLLECTION_NAMESPACE=${{ env.collection_namespace }} make build_collection
COLLECTION_TEMPLATE_VERSION=true COLLECTION_NAMESPACE=${{ env.collection_namespace }} make build_collection
ansible-galaxy collection publish \
--token=${{ secrets.GALAXY_TOKEN }} \
awx_collection_build/${{ env.collection_namespace }}-awx-${{ github.event.release.tag_name }}.tar.gz
@@ -70,4 +70,4 @@ jobs:
docker tag ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} quay.io/${{ github.repository }}:latest
docker push quay.io/${{ github.repository }}:${{ github.event.release.tag_name }}
docker push quay.io/${{ github.repository }}:latest

View File

@@ -100,23 +100,10 @@ jobs:
AWX_TEST_IMAGE: ${{ github.repository }}
AWX_TEST_VERSION: ${{ github.event.inputs.version }}
- name: Generate changelog
uses: shanemcd/simple-changelog-generator@v1
id: changelog
with:
repo: "${{ github.repository }}"
- name: Write changelog to file
run: |
cat << 'EOF' > /tmp/awx-changelog
${{ steps.changelog.outputs.changelog }}
EOF
- name: Create draft release for AWX
working-directory: awx
run: |
ansible-playbook -v tools/ansible/stage.yml \
-e changelog_path=/tmp/awx-changelog \
-e repo=${{ github.repository }} \
-e awx_image=ghcr.io/${{ github.repository }} \
-e version=${{ github.event.inputs.version }} \

View File

@@ -0,0 +1,26 @@
---
name: Dependency Pr Update
on:
pull_request:
types: [labeled, opened, reopened]
jobs:
pr-check:
name: Update Dependabot Prs
if: contains(github.event.pull_request.labels.*.name, 'dependencies') && contains(github.event.pull_request.labels.*.name, 'component:ui')
runs-on: ubuntu-latest
steps:
- name: Checkout branch
uses: actions/checkout@v3
- name: Update PR Body
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.event.repository.name }}
BRANCH: ${{github.event.pull_request.head.ref}}
PR: ${{github.event.pull_request}}
run: |
gh pr checkout ${{ env.BRANCH }}
gh pr edit --body "${{ env.PR }}\nBug, Docs Fix or other nominal change"

1
.gitignore vendored
View File

@@ -38,7 +38,6 @@ awx/ui/build
awx/ui/.env.local
awx/ui/instrumented
rsyslog.pid
tools/prometheus/data
tools/docker-compose/ansible/awx_dump.sql
tools/docker-compose/Dockerfile
tools/docker-compose/_build

View File

@@ -19,16 +19,17 @@ Have questions about this document or anything not covered here? Come chat with
- [Purging containers and images](#purging-containers-and-images)
- [Pre commit hooks](#pre-commit-hooks)
- [What should I work on?](#what-should-i-work-on)
- [Translations](#translations)
- [Submitting Pull Requests](#submitting-pull-requests)
- [PR Checks run by Zuul](#pr-checks-run-by-zuul)
- [Reporting Issues](#reporting-issues)
- [Getting Help](#getting-help)
## Things to know prior to submitting code
- All code submissions are done through pull requests against the `devel` branch.
- You must use `git commit --signoff` for any commit to be merged, and agree that usage of --signoff constitutes agreement with the terms of [DCO 1.1](./DCO_1_1.md).
- Take care to make sure no merge commits are in the submission, and use `git rebase` vs `git merge` for this reason.
- If collaborating with someone else on the same branch, consider using `--force-with-lease` instead of `--force`. This will prevent you from accidentally overwriting commits pushed by someone else. For more information, see https://git-scm.com/docs/git-push#git-push---force-with-leaseltrefnamegt
- If collaborating with someone else on the same branch, consider using `--force-with-lease` instead of `--force`. This will prevent you from accidentally overwriting commits pushed by someone else. For more information, see [git push docs](https://git-scm.com/docs/git-push#git-push---force-with-leaseltrefnamegt).
- If submitting a large code change, it's a good idea to join the `#ansible-awx` channel on irc.libera.chat, and talk about what you would like to do or add first. This not only helps everyone know what's going on, it also helps save time and effort, if the community decides some changes are needed.
- We ask all of our community members and contributors to adhere to the [Ansible code of conduct](http://docs.ansible.com/ansible/latest/community/code_of_conduct.html). If you have questions, or need assistance, please reach out to our community team at [codeofconduct@ansible.com](mailto:codeofconduct@ansible.com)
@@ -42,8 +43,7 @@ The AWX development environment workflow and toolchain uses Docker and the docke
Prior to starting the development services, you'll need `docker` and `docker-compose`. On Linux, you can generally find these in your distro's packaging, but you may find that Docker themselves maintain a separate repo that tracks more closely to the latest releases.
For macOS and Windows, we recommend [Docker for Mac](https://www.docker.com/docker-mac) and [Docker for Windows](https://www.docker.com/docker-windows)
respectively.
For macOS and Windows, we recommend [Docker for Mac](https://www.docker.com/docker-mac) and [Docker for Windows](https://www.docker.com/docker-windows) respectively.
For Linux platforms, refer to the following from Docker:
@@ -79,17 +79,13 @@ See the [README.md](./tools/docker-compose/README.md) for docs on how to build t
### Building API Documentation
AWX includes support for building [Swagger/OpenAPI
documentation](https://swagger.io). To build the documentation locally, run:
AWX includes support for building [Swagger/OpenAPI documentation](https://swagger.io). To build the documentation locally, run:
```bash
(container)/awx_devel$ make swagger
```
This will write a file named `swagger.json` that contains the API specification
in OpenAPI format. A variety of online tools are available for translating
this data into more consumable formats (such as HTML). http://editor.swagger.io
is an example of one such service.
This will write a file named `swagger.json` that contains the API specification in OpenAPI format. A variety of online tools are available for translating this data into more consumable formats (such as HTML). http://editor.swagger.io is an example of one such service.
### Accessing the AWX web interface
@@ -115,20 +111,30 @@ While you can use environment variables to skip the pre-commit hooks GitHub will
## What should I work on?
We have a ["good first issue" label](https://github.com/ansible/awx/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) we put on some issues that might be a good starting point for new contributors.
Fixing bugs and updating the documentation are always appreciated, so reviewing the backlog of issues is always a good place to start.
For feature work, take a look at the current [Enhancements](https://github.com/ansible/awx/issues?q=is%3Aissue+is%3Aopen+label%3Atype%3Aenhancement).
If it has someone assigned to it then that person is the person responsible for working the enhancement. If you feel like you could contribute then reach out to that person.
Fixing bugs, adding translations, and updating the documentation are always appreciated, so reviewing the backlog of issues is always a good place to start. For extra information on debugging tools, see [Debugging](./docs/debugging/).
**NOTES**
> Issue assignment will only be done for maintainers of the project. If you decide to work on an issue, please feel free to add a comment in the issue to let others know that you are working on it; but know that we will accept the first pull request from whomever is able to fix an issue. Once your PR is accepted we can add you as an assignee to an issue upon request.
**NOTE**
> If you work in a part of the codebase that is going through active development, your changes may be rejected, or you may be asked to `rebase`. A good idea before starting work is to have a discussion with us in the `#ansible-awx` channel on irc.libera.chat, or on the [mailing list](https://groups.google.com/forum/#!forum/awx-project).
**NOTE**
> If you're planning to develop features or fixes for the UI, please review the [UI Developer doc](./awx/ui/README.md).
### Translations
At this time we do not accept PRs for adding additional language translations as we have an automated process for generating our translations. This is because translations require constant care as new strings are added and changed in the code base. Because of this the .po files are overwritten during every translation release cycle. We also can't support a lot of translations on AWX as its an open source project and each language adds time and cost to maintain. If you would like to see AWX translated into a new language please create an issue and ask others you know to upvote the issue. Our translation team will review the needs of the community and see what they can do around supporting additional language.
If you find an issue with an existing translation, please see the [Reporting Issues](#reporting-issues) section to open an issue and our translation team will work with you on a resolution.
## Submitting Pull Requests
Fixes and Features for AWX will go through the Github pull request process. Submit your pull request (PR) against the `devel` branch.
@@ -152,28 +158,14 @@ We like to keep our commit history clean, and will require resubmission of pull
Sometimes it might take us a while to fully review your PR. We try to keep the `devel` branch in good working order, and so we review requests carefully. Please be patient.
All submitted PRs will have the linter and unit tests run against them via Zuul, and the status reported in the PR.
## PR Checks run by Zuul
Zuul jobs for awx are defined in the [zuul-jobs](https://github.com/ansible/zuul-jobs) repo.
Zuul runs the following checks that must pass:
1. `tox-awx-api-lint`
2. `tox-awx-ui-lint`
3. `tox-awx-api`
4. `tox-awx-ui`
5. `tox-awx-swagger`
Zuul runs the following checks that are non-voting (can not pass but serve to inform PR reviewers):
1. `tox-awx-detect-schema-change`
This check generates the schema and diffs it against a reference copy of the `devel` version of the schema.
Reviewers should inspect the `job-output.txt.gz` related to the check if their is a failure (grep for `diff -u -b` to find beginning of diff).
If the schema change is expected and makes sense in relation to the changes made by the PR, then you are good to go!
If not, the schema changes should be fixed, but this decision must be enforced by reviewers.
When your PR is initially submitted the checks will not be run until a maintainer allows them to be. Once a maintainer has done a quick review of your work the PR will have the linter and unit tests run against them via GitHub Actions, and the status reported in the PR.
## Reporting Issues
We welcome your feedback, and encourage you to file an issue when you run into a problem. But before opening a new issues, we ask that you please view our [Issues guide](./ISSUES.md).
## Getting Help
If you require additional assistance, please reach out to us at `#ansible-awx` on irc.libera.chat, or submit your question to the [mailing list](https://groups.google.com/forum/#!forum/awx-project).
For extra information on debugging tools, see [Debugging](./docs/debugging/).

134
Makefile
View File

@@ -5,8 +5,8 @@ NPM_BIN ?= npm
CHROMIUM_BIN=/tmp/chrome-linux/chrome
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
MANAGEMENT_COMMAND ?= awx-manage
VERSION := $(shell $(PYTHON) setup.py --version)
COLLECTION_VERSION := $(shell $(PYTHON) setup.py --version | cut -d . -f 1-3)
VERSION := $(shell $(PYTHON) tools/scripts/scm_version.py)
COLLECTION_VERSION := $(shell $(PYTHON) tools/scripts/scm_version.py | cut -d . -f 1-3)
# NOTE: This defaults the container image version to the branch that's active
COMPOSE_TAG ?= $(GIT_BRANCH)
@@ -15,6 +15,12 @@ MAIN_NODE_TYPE ?= hybrid
KEYCLOAK ?= false
# If set to true docker-compose will also start an ldap instance
LDAP ?= false
# If set to true docker-compose will also start a splunk instance
SPLUNK ?= false
# If set to true docker-compose will also start a prometheus instance
PROMETHEUS ?= false
# If set to true docker-compose will also start a grafana instance
GRAFANA ?= false
VENV_BASE ?= /var/lib/awx/venv
@@ -43,7 +49,7 @@ I18N_FLAG_FILE = .i18n_built
.PHONY: awx-link clean clean-tmp clean-venv requirements requirements_dev \
develop refresh adduser migrate dbchange \
receiver test test_unit test_coverage coverage_html \
dev_build release_build sdist \
sdist \
ui-release ui-devel \
VERSION PYTHON_VERSION docker-compose-sources \
.git/hooks/pre-commit
@@ -66,7 +72,7 @@ clean-languages:
rm -f $(I18N_FLAG_FILE)
find ./awx/locale/ -type f -regex ".*\.mo$" -delete
# Remove temporary build files, compiled Python files.
## Remove temporary build files, compiled Python files.
clean: clean-ui clean-api clean-awxkit clean-dist
rm -rf awx/public
rm -rf awx/lib/site-packages
@@ -88,7 +94,7 @@ clean-api:
clean-awxkit:
rm -rf awxkit/*.egg-info awxkit/.tox awxkit/build/*
# convenience target to assert environment variables are defined
## convenience target to assert environment variables are defined
guard-%:
@if [ "$${$*}" = "" ]; then \
echo "The required environment variable '$*' is not set"; \
@@ -111,7 +117,7 @@ virtualenv_awx:
fi; \
fi
# Install third-party requirements needed for AWX's environment.
## Install third-party requirements needed for AWX's environment.
# this does not use system site packages intentionally
requirements_awx: virtualenv_awx
if [[ "$(PIP_OPTIONS)" == *"--no-index"* ]]; then \
@@ -130,7 +136,7 @@ requirements_dev: requirements_awx requirements_awx_dev
requirements_test: requirements
# "Install" awx package in development mode.
## "Install" awx package in development mode.
develop:
@if [ "$(VIRTUAL_ENV)" ]; then \
pip uninstall -y awx; \
@@ -147,21 +153,21 @@ version_file:
fi; \
$(PYTHON) -c "import awx; print(awx.__version__)" > /var/lib/awx/.awx_version; \
# Refresh development environment after pulling new code.
## Refresh development environment after pulling new code.
refresh: clean requirements_dev version_file develop migrate
# Create Django superuser.
## Create Django superuser.
adduser:
$(MANAGEMENT_COMMAND) createsuperuser
# Create database tables and apply any new migrations.
## Create database tables and apply any new migrations.
migrate:
if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/awx/bin/activate; \
fi; \
$(MANAGEMENT_COMMAND) migrate --noinput
# Run after making changes to the models to create a new migration.
## Run after making changes to the models to create a new migration.
dbchange:
$(MANAGEMENT_COMMAND) makemigrations
@@ -198,7 +204,7 @@ uwsgi: collectstatic
--logformat "%(addr) %(method) %(uri) - %(proto) %(status)"
awx-autoreload:
@/awx_devel/tools/docker-compose/awx-autoreload /awx_devel "$(DEV_RELOAD_COMMAND)"
@/awx_devel/tools/docker-compose/awx-autoreload /awx_devel/awx "$(DEV_RELOAD_COMMAND)"
daphne:
@if [ "$(VENV_BASE)" ]; then \
@@ -212,7 +218,7 @@ wsbroadcast:
fi; \
$(PYTHON) manage.py run_wsbroadcast
# Run to start the background task dispatcher for development.
## Run to start the background task dispatcher for development.
dispatcher:
@if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/awx/bin/activate; \
@@ -220,7 +226,7 @@ dispatcher:
$(PYTHON) manage.py run_dispatcher
# Run to start the zeromq callback receiver
## Run to start the zeromq callback receiver
receiver:
@if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/awx/bin/activate; \
@@ -267,12 +273,12 @@ api-lint:
yamllint -s .
awx-link:
[ -d "/awx_devel/awx.egg-info" ] || $(PYTHON) /awx_devel/setup.py egg_info_dev
[ -d "/awx_devel/awx.egg-info" ] || $(PYTHON) /awx_devel/tools/scripts/egg_info_dev
cp -f /tmp/awx.egg-link /var/lib/awx/venv/awx/lib/$(PYTHON)/site-packages/awx.egg-link
TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests
PYTEST_ARGS ?= -n auto
# Run all API unit tests.
## Run all API unit tests.
test:
if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/awx/bin/activate; \
@@ -286,6 +292,7 @@ COLLECTION_TEST_TARGET ?=
COLLECTION_PACKAGE ?= awx
COLLECTION_NAMESPACE ?= awx
COLLECTION_INSTALL = ~/.ansible/collections/ansible_collections/$(COLLECTION_NAMESPACE)/$(COLLECTION_PACKAGE)
COLLECTION_TEMPLATE_VERSION ?= false
test_collection:
rm -f $(shell ls -d $(VENV_BASE)/awx/lib/python* | head -n 1)/no-global-site-packages.txt
@@ -313,7 +320,7 @@ awx_collection_build: $(shell find awx_collection -type f)
-e collection_package=$(COLLECTION_PACKAGE) \
-e collection_namespace=$(COLLECTION_NAMESPACE) \
-e collection_version=$(COLLECTION_VERSION) \
-e '{"awx_template_version":false}'
-e '{"awx_template_version": $(COLLECTION_TEMPLATE_VERSION)}'
ansible-galaxy collection build awx_collection_build --force --output-path=awx_collection_build
build_collection: awx_collection_build
@@ -334,23 +341,24 @@ test_unit:
fi; \
py.test awx/main/tests/unit awx/conf/tests/unit awx/sso/tests/unit
# Run all API unit tests with coverage enabled.
## Run all API unit tests with coverage enabled.
test_coverage:
@if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/awx/bin/activate; \
fi; \
py.test --create-db --cov=awx --cov-report=xml --junitxml=./reports/junit.xml $(TEST_DIRS)
# Output test coverage as HTML (into htmlcov directory).
## Output test coverage as HTML (into htmlcov directory).
coverage_html:
coverage html
# Run API unit tests across multiple Python/Django versions with Tox.
## Run API unit tests across multiple Python/Django versions with Tox.
test_tox:
tox -v
# Make fake data
DATA_GEN_PRESET = ""
## Make fake data
bulk_data:
@if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/awx/bin/activate; \
@@ -373,7 +381,8 @@ clean-ui:
awx/ui/node_modules:
NODE_OPTIONS=--max-old-space-size=6144 $(NPM_BIN) --prefix awx/ui --loglevel warn ci
$(UI_BUILD_FLAG_FILE): awx/ui/node_modules
$(UI_BUILD_FLAG_FILE):
$(MAKE) awx/ui/node_modules
$(PYTHON) tools/scripts/compilemessages.py
$(NPM_BIN) --prefix awx/ui --loglevel warn run compile-strings
$(NPM_BIN) --prefix awx/ui --loglevel warn run build
@@ -417,21 +426,13 @@ ui-test-general:
$(NPM_BIN) run --prefix awx/ui pretest
$(NPM_BIN) run --prefix awx/ui/ test-general --runInBand
# Build a pip-installable package into dist/ with a timestamped version number.
dev_build:
$(PYTHON) setup.py dev_build
# Build a pip-installable package into dist/ with the release version number.
release_build:
$(PYTHON) setup.py release_build
HEADLESS ?= no
ifeq ($(HEADLESS), yes)
dist/$(SDIST_TAR_FILE):
else
dist/$(SDIST_TAR_FILE): $(UI_BUILD_FLAG_FILE)
endif
$(PYTHON) setup.py $(SDIST_COMMAND)
$(PYTHON) -m build -s
ln -sf $(SDIST_TAR_FILE) dist/awx.tar.gz
sdist: dist/$(SDIST_TAR_FILE)
@@ -452,6 +453,11 @@ COMPOSE_OPTS ?=
CONTROL_PLANE_NODE_COUNT ?= 1
EXECUTION_NODE_COUNT ?= 2
MINIKUBE_CONTAINER_GROUP ?= false
EXTRA_SOURCES_ANSIBLE_OPTS ?=
ifneq ($(ADMIN_PASSWORD),)
EXTRA_SOURCES_ANSIBLE_OPTS := -e admin_password=$(ADMIN_PASSWORD) $(EXTRA_SOURCES_ANSIBLE_OPTS)
endif
docker-compose-sources: .git/hooks/pre-commit
@if [ $(MINIKUBE_CONTAINER_GROUP) = true ]; then\
@@ -466,7 +472,11 @@ docker-compose-sources: .git/hooks/pre-commit
-e execution_node_count=$(EXECUTION_NODE_COUNT) \
-e minikube_container_group=$(MINIKUBE_CONTAINER_GROUP) \
-e enable_keycloak=$(KEYCLOAK) \
-e enable_ldap=$(LDAP)
-e enable_ldap=$(LDAP) \
-e enable_splunk=$(SPLUNK) \
-e enable_prometheus=$(PROMETHEUS) \
-e enable_grafana=$(GRAFANA) $(EXTRA_SOURCES_ANSIBLE_OPTS)
docker-compose: awx/projects docker-compose-sources
@@ -500,7 +510,7 @@ docker-compose-container-group-clean:
fi
rm -rf tools/docker-compose-minikube/_sources/
# Base development image build
## Base development image build
docker-compose-build:
ansible-playbook tools/ansible/dockerfile.yml -e build_dev=True -e receptor_image=$(RECEPTOR_IMAGE)
DOCKER_BUILDKIT=1 docker build -t $(DEVEL_IMAGE_NAME) \
@@ -514,20 +524,17 @@ docker-clean:
fi
docker-clean-volumes: docker-compose-clean docker-compose-container-group-clean
docker volume rm tools_awx_db
docker volume rm -f tools_awx_db tools_grafana_storage tools_prometheus_storage $(docker volume ls --filter name=tools_redis_socket_ -q)
docker-refresh: docker-clean docker-compose
# Docker Development Environment with Elastic Stack Connected
## Docker Development Environment with Elastic Stack Connected
docker-compose-elk: awx/projects docker-compose-sources
docker-compose -f tools/docker-compose/_sources/docker-compose.yml -f tools/elastic/docker-compose.logstash-link.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate
docker-compose-cluster-elk: awx/projects docker-compose-sources
docker-compose -f tools/docker-compose/_sources/docker-compose.yml -f tools/elastic/docker-compose.logstash-link-cluster.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate
prometheus:
docker run -u0 --net=tools_default --link=`docker ps | egrep -o "tools_awx(_run)?_([^ ]+)?"`:awxweb --volume `pwd`/tools/prometheus:/prometheus --name prometheus -d -p 0.0.0.0:9090:9090 prom/prometheus --web.enable-lifecycle --config.file=/prometheus/prometheus.yml
docker-compose-container-group:
MINIKUBE_CONTAINER_GROUP=true make docker-compose
@@ -558,26 +565,34 @@ Dockerfile.kube-dev: tools/ansible/roles/dockerfile/templates/Dockerfile.j2
-e template_dest=_build_kube_dev \
-e receptor_image=$(RECEPTOR_IMAGE)
## Build awx_kube_devel image for development on local Kubernetes environment.
awx-kube-dev-build: Dockerfile.kube-dev
DOCKER_BUILDKIT=1 docker build -f Dockerfile.kube-dev \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--cache-from=$(DEV_DOCKER_TAG_BASE)/awx_kube_devel:$(COMPOSE_TAG) \
-t $(DEV_DOCKER_TAG_BASE)/awx_kube_devel:$(COMPOSE_TAG) .
## Build awx image for deployment on Kubernetes environment.
awx-kube-build: Dockerfile
DOCKER_BUILDKIT=1 docker build -f Dockerfile \
--build-arg VERSION=$(VERSION) \
--build-arg SETUPTOOLS_SCM_PRETEND_VERSION=$(VERSION) \
--build-arg HEADLESS=$(HEADLESS) \
-t $(DEV_DOCKER_TAG_BASE)/awx:$(COMPOSE_TAG) .
# Translation TASKS
# --------------------------------------
# generate UI .pot file, an empty template of strings yet to be translated
## generate UI .pot file, an empty template of strings yet to be translated
pot: $(UI_BUILD_FLAG_FILE)
$(NPM_BIN) --prefix awx/ui --loglevel warn run extract-template --clean
# generate UI .po files for each locale (will update translated strings for `en`)
## generate UI .po files for each locale (will update translated strings for `en`)
po: $(UI_BUILD_FLAG_FILE)
$(NPM_BIN) --prefix awx/ui --loglevel warn run extract-strings -- --clean
# generate API django .pot .po
LANG = "en-us"
LANG = "en_us"
## generate API django .pot .po
messages:
@if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/awx/bin/activate; \
@@ -586,3 +601,38 @@ messages:
print-%:
@echo $($*)
# HELP related targets
# --------------------------------------
HELP_FILTER=.PHONY
## Display help targets
help:
@printf "Available targets:\n"
@make -s help/generate | grep -vE "\w($(HELP_FILTER))"
## Display help for all targets
help/all:
@printf "Available targets:\n"
@make -s help/generate
## Generate help output from MAKEFILE_LIST
help/generate:
@awk '/^[-a-zA-Z_0-9%:\\\.\/]+:/ { \
helpMessage = match(lastLine, /^## (.*)/); \
if (helpMessage) { \
helpCommand = $$1; \
helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \
gsub("\\\\", "", helpCommand); \
gsub(":+$$", "", helpCommand); \
printf " \x1b[32;01m%-35s\x1b[0m %s\n", helpCommand, helpMessage; \
} else { \
helpCommand = $$1; \
gsub("\\\\", "", helpCommand); \
gsub(":+$$", "", helpCommand); \
printf " \x1b[32;01m%-35s\x1b[0m %s\n", helpCommand, "No help available"; \
} \
} \
{ lastLine = $$0 }' $(MAKEFILE_LIST) | sort -u
@printf "\n"

View File

@@ -6,9 +6,40 @@ import os
import sys
import warnings
from pkg_resources import get_distribution
__version__ = get_distribution('awx').version
def get_version():
version_from_file = get_version_from_file()
if version_from_file:
return version_from_file
else:
from setuptools_scm import get_version
version = get_version(root='..', relative_to=__file__)
return version
def get_version_from_file():
vf = version_file()
if vf:
with open(vf, 'r') as file:
return file.read().strip()
def version_file():
current_dir = os.path.dirname(os.path.abspath(__file__))
version_file = os.path.join(current_dir, '..', 'VERSION')
if os.path.exists(version_file):
return version_file
try:
import pkg_resources
__version__ = pkg_resources.get_distribution('awx').version
except pkg_resources.DistributionNotFound:
__version__ = get_version()
__all__ = ['__version__']
@@ -21,7 +52,6 @@ try:
except ImportError: # pragma: no cover
MODE = 'production'
import hashlib
try:
@@ -78,9 +108,10 @@ def oauth2_getattribute(self, attr):
# Custom method to override
# oauth2_provider.settings.OAuth2ProviderSettings.__getattribute__
from django.conf import settings
from oauth2_provider.settings import DEFAULTS
val = None
if 'migrate' not in sys.argv:
if (isinstance(attr, str)) and (attr in DEFAULTS) and (not attr.startswith('_')):
# certain Django OAuth Toolkit migrations actually reference
# setting lookups for references to model classes (e.g.,
# oauth2_settings.REFRESH_TOKEN_MODEL)
@@ -159,7 +190,7 @@ def manage():
sys.stdout.write('%s\n' % __version__)
# If running as a user without permission to read settings, display an
# error message. Allow --help to still work.
elif settings.SECRET_KEY == 'permission-denied':
elif not os.getenv('SKIP_SECRET_KEY_CHECK', False) and settings.SECRET_KEY == 'permission-denied':
if len(sys.argv) == 1 or len(sys.argv) >= 2 and sys.argv[1] in ('-h', '--help', 'help'):
execute_from_command_line(sys.argv)
sys.stdout.write('\n')

View File

@@ -157,7 +157,7 @@ class FieldLookupBackend(BaseFilterBackend):
# A list of fields that we know can be filtered on without the possiblity
# of introducing duplicates
NO_DUPLICATES_ALLOW_LIST = (CharField, IntegerField, BooleanField)
NO_DUPLICATES_ALLOW_LIST = (CharField, IntegerField, BooleanField, TextField)
def get_fields_from_lookup(self, model, lookup):
@@ -232,6 +232,9 @@ class FieldLookupBackend(BaseFilterBackend):
re.compile(value)
except re.error as e:
raise ValueError(e.args[0])
elif new_lookup.endswith('__iexact'):
if not isinstance(field, (CharField, TextField)):
raise ValueError(f'{field.name} is not a text field and cannot be filtered by case-insensitive search')
elif new_lookup.endswith('__search'):
related_model = getattr(field, 'related_model', None)
if not related_model:
@@ -258,8 +261,8 @@ class FieldLookupBackend(BaseFilterBackend):
search_filters = {}
needs_distinct = False
# Can only have two values: 'AND', 'OR'
# If 'AND' is used, an iterm must satisfy all condition to show up in the results.
# If 'OR' is used, an item just need to satisfy one condition to appear in results.
# If 'AND' is used, an item must satisfy all conditions to show up in the results.
# If 'OR' is used, an item just needs to satisfy one condition to appear in results.
search_filter_relation = 'OR'
for key, values in request.query_params.lists():
if key in self.RESERVED_NAMES:

View File

@@ -29,7 +29,6 @@ from django.utils.translation import gettext_lazy as _
from django.utils.encoding import force_str
from django.utils.text import capfirst
from django.utils.timezone import now
from django.utils.functional import cached_property
# Django REST Framework
from rest_framework.exceptions import ValidationError, PermissionDenied
@@ -155,6 +154,7 @@ SUMMARIZABLE_FK_FIELDS = {
'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'),
'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed'),
'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'kubernetes', 'credential_type_id'),
'signature_validation_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'credential_type_id'),
'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'elapsed', 'type', 'canceled_on'),
'job_template': DEFAULT_SUMMARY_FIELDS,
'workflow_job_template': DEFAULT_SUMMARY_FIELDS,
@@ -1471,6 +1471,7 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
'allow_override',
'custom_virtualenv',
'default_environment',
'signature_validation_credential',
) + (
'last_update_failed',
'last_updated',
@@ -1607,7 +1608,6 @@ class ProjectUpdateSerializer(UnifiedJobSerializer, ProjectOptionsSerializer):
class ProjectUpdateDetailSerializer(ProjectUpdateSerializer):
host_status_counts = serializers.SerializerMethodField(help_text=_('A count of hosts uniquely assigned to each status.'))
playbook_counts = serializers.SerializerMethodField(help_text=_('A count of all plays and tasks for the job run.'))
class Meta:
@@ -1622,14 +1622,6 @@ class ProjectUpdateDetailSerializer(ProjectUpdateSerializer):
return data
def get_host_status_counts(self, obj):
try:
counts = obj.project_update_events.only('event_data').get(event='playbook_on_stats').get_host_status_counts()
except ProjectUpdateEvent.DoesNotExist:
counts = {}
return counts
class ProjectUpdateListSerializer(ProjectUpdateSerializer, UnifiedJobListSerializer):
class Meta:
@@ -2082,7 +2074,7 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
class Meta:
model = InventorySource
fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout', 'source_project', 'update_on_project_update') + (
fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout', 'source_project') + (
'last_update_failed',
'last_updated',
) # Backwards compatibility.
@@ -2145,11 +2137,6 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
raise serializers.ValidationError(_("Cannot use manual project for SCM-based inventory."))
return value
def validate_update_on_project_update(self, value):
if value and self.instance and self.instance.schedules.exists():
raise serializers.ValidationError(_("Setting not compatible with existing schedules."))
return value
def validate_inventory(self, value):
if value and value.kind == 'smart':
raise serializers.ValidationError({"detail": _("Cannot create Inventory Source for Smart Inventory")})
@@ -2200,7 +2187,7 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
if ('source' in attrs or 'source_project' in attrs) and get_field_from_model_or_attrs('source_project') is None:
raise serializers.ValidationError({"source_project": _("Project required for scm type sources.")})
else:
redundant_scm_fields = list(filter(lambda x: attrs.get(x, None), ['source_project', 'source_path', 'update_on_project_update']))
redundant_scm_fields = list(filter(lambda x: attrs.get(x, None), ['source_project', 'source_path']))
if redundant_scm_fields:
raise serializers.ValidationError({"detail": _("Cannot set %s if not SCM type." % ' '.join(redundant_scm_fields))})
@@ -2245,7 +2232,6 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri
'source_project_update',
'custom_virtualenv',
'instance_group',
'-controller_node',
)
def get_related(self, obj):
@@ -2320,7 +2306,6 @@ class InventoryUpdateDetailSerializer(InventoryUpdateSerializer):
class InventoryUpdateListSerializer(InventoryUpdateSerializer, UnifiedJobListSerializer):
class Meta:
model = InventoryUpdate
fields = ('*', '-controller_node') # field removal undone by UJ serializer
class InventoryUpdateCancelSerializer(InventoryUpdateSerializer):
@@ -2673,6 +2658,13 @@ class CredentialSerializer(BaseSerializer):
return credential_type
def validate_inputs(self, inputs):
if self.instance and self.instance.credential_type.kind == "vault":
if 'vault_id' in inputs and inputs['vault_id'] != self.instance.inputs['vault_id']:
raise ValidationError(_('Vault IDs cannot be changed once they have been created.'))
return inputs
class CredentialSerializerCreate(CredentialSerializer):
@@ -3107,7 +3099,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
class JobDetailSerializer(JobSerializer):
host_status_counts = serializers.SerializerMethodField(help_text=_('A count of hosts uniquely assigned to each status.'))
playbook_counts = serializers.SerializerMethodField(help_text=_('A count of all plays and tasks for the job run.'))
custom_virtualenv = serializers.ReadOnlyField()
@@ -3123,14 +3114,6 @@ class JobDetailSerializer(JobSerializer):
return data
def get_host_status_counts(self, obj):
try:
counts = obj.get_event_queryset().only('event_data').get(event='playbook_on_stats').get_host_status_counts()
except JobEvent.DoesNotExist:
counts = {}
return counts
class JobCancelSerializer(BaseSerializer):
@@ -3319,21 +3302,10 @@ class AdHocCommandSerializer(UnifiedJobSerializer):
class AdHocCommandDetailSerializer(AdHocCommandSerializer):
host_status_counts = serializers.SerializerMethodField(help_text=_('A count of hosts uniquely assigned to each status.'))
class Meta:
model = AdHocCommand
fields = ('*', 'host_status_counts')
def get_host_status_counts(self, obj):
try:
counts = obj.ad_hoc_command_events.only('event_data').get(event='playbook_on_stats').get_host_status_counts()
except AdHocCommandEvent.DoesNotExist:
counts = {}
return counts
class AdHocCommandCancelSerializer(AdHocCommandSerializer):
@@ -4225,6 +4197,15 @@ class JobLaunchSerializer(BaseSerializer):
elif template.project.status in ('error', 'failed'):
errors['playbook'] = _("Missing a revision to run due to failed project update.")
latest_update = template.project.project_updates.last()
if latest_update is not None and latest_update.failed:
failed_validation_tasks = latest_update.project_update_events.filter(
event='runner_on_failed',
play="Perform project signature/checksum verification",
)
if failed_validation_tasks:
errors['playbook'] = _("Last project update failed due to signature validation failure.")
# cannot run a playbook without an inventory
if template.inventory and template.inventory.pending_deletion is True:
errors['inventory'] = _("The inventory associated with this Job Template is being deleted.")
@@ -4502,7 +4483,10 @@ class NotificationTemplateSerializer(BaseSerializer):
body = messages[event].get('body', {})
if body:
try:
potential_body = json.loads(body)
rendered_body = (
sandbox.ImmutableSandboxedEnvironment(undefined=DescriptiveUndefined).from_string(body).render(JobNotificationMixin.context_stub())
)
potential_body = json.loads(rendered_body)
if not isinstance(potential_body, dict):
error_list.append(
_("Webhook body for '{}' should be a json dictionary. Found type '{}'.".format(event, type(potential_body).__name__))
@@ -4645,69 +4629,74 @@ class SchedulePreviewSerializer(BaseSerializer):
# We reject rrules if:
# - DTSTART is not include
# - INTERVAL is not included
# - SECONDLY is used
# - TZID is used
# - BYDAY prefixed with a number (MO is good but not 20MO)
# - BYYEARDAY
# - BYWEEKNO
# - Multiple DTSTART or RRULE elements
# - Can't contain both COUNT and UNTIL
# - COUNT > 999
# - Multiple DTSTART
# - At least one of RRULE is not included
# - EXDATE or RDATE is included
# For any rule in the ruleset:
# - INTERVAL is not included
# - SECONDLY is used
# - BYDAY prefixed with a number (MO is good but not 20MO)
# - Can't contain both COUNT and UNTIL
# - COUNT > 999
def validate_rrule(self, value):
rrule_value = value
multi_by_month_day = r".*?BYMONTHDAY[\:\=][0-9]+,-*[0-9]+"
multi_by_month = r".*?BYMONTH[\:\=][0-9]+,[0-9]+"
by_day_with_numeric_prefix = r".*?BYDAY[\:\=][0-9]+[a-zA-Z]{2}"
match_count = re.match(r".*?(COUNT\=[0-9]+)", rrule_value)
match_multiple_dtstart = re.findall(r".*?(DTSTART(;[^:]+)?\:[0-9]+T[0-9]+Z?)", rrule_value)
match_native_dtstart = re.findall(r".*?(DTSTART:[0-9]+T[0-9]+) ", rrule_value)
match_multiple_rrule = re.findall(r".*?(RRULE\:)", rrule_value)
match_multiple_rrule = re.findall(r".*?(RULE\:[^\s]*)", rrule_value)
errors = []
if not len(match_multiple_dtstart):
raise serializers.ValidationError(_('Valid DTSTART required in rrule. Value should start with: DTSTART:YYYYMMDDTHHMMSSZ'))
errors.append(_('Valid DTSTART required in rrule. Value should start with: DTSTART:YYYYMMDDTHHMMSSZ'))
if len(match_native_dtstart):
raise serializers.ValidationError(_('DTSTART cannot be a naive datetime. Specify ;TZINFO= or YYYYMMDDTHHMMSSZZ.'))
errors.append(_('DTSTART cannot be a naive datetime. Specify ;TZINFO= or YYYYMMDDTHHMMSSZZ.'))
if len(match_multiple_dtstart) > 1:
raise serializers.ValidationError(_('Multiple DTSTART is not supported.'))
if not len(match_multiple_rrule):
raise serializers.ValidationError(_('RRULE required in rrule.'))
if len(match_multiple_rrule) > 1:
raise serializers.ValidationError(_('Multiple RRULE is not supported.'))
if 'interval' not in rrule_value.lower():
raise serializers.ValidationError(_('INTERVAL required in rrule.'))
if 'secondly' in rrule_value.lower():
raise serializers.ValidationError(_('SECONDLY is not supported.'))
if re.match(multi_by_month_day, rrule_value):
raise serializers.ValidationError(_('Multiple BYMONTHDAYs not supported.'))
if re.match(multi_by_month, rrule_value):
raise serializers.ValidationError(_('Multiple BYMONTHs not supported.'))
if re.match(by_day_with_numeric_prefix, rrule_value):
raise serializers.ValidationError(_("BYDAY with numeric prefix not supported."))
if 'byyearday' in rrule_value.lower():
raise serializers.ValidationError(_("BYYEARDAY not supported."))
if 'byweekno' in rrule_value.lower():
raise serializers.ValidationError(_("BYWEEKNO not supported."))
if 'COUNT' in rrule_value and 'UNTIL' in rrule_value:
raise serializers.ValidationError(_("RRULE may not contain both COUNT and UNTIL"))
if match_count:
count_val = match_count.groups()[0].strip().split("=")
if int(count_val[1]) > 999:
raise serializers.ValidationError(_("COUNT > 999 is unsupported."))
errors.append(_('Multiple DTSTART is not supported.'))
if "rrule:" not in rrule_value.lower():
errors.append(_('One or more rule required in rrule.'))
if "exdate:" in rrule_value.lower():
raise serializers.ValidationError(_('EXDATE not allowed in rrule.'))
if "rdate:" in rrule_value.lower():
raise serializers.ValidationError(_('RDATE not allowed in rrule.'))
for a_rule in match_multiple_rrule:
if 'interval' not in a_rule.lower():
errors.append("{0}: {1}".format(_('INTERVAL required in rrule'), a_rule))
elif 'secondly' in a_rule.lower():
errors.append("{0}: {1}".format(_('SECONDLY is not supported'), a_rule))
if re.match(by_day_with_numeric_prefix, a_rule):
errors.append("{0}: {1}".format(_("BYDAY with numeric prefix not supported"), a_rule))
if 'COUNT' in a_rule and 'UNTIL' in a_rule:
errors.append("{0}: {1}".format(_("RRULE may not contain both COUNT and UNTIL"), a_rule))
match_count = re.match(r".*?(COUNT\=[0-9]+)", a_rule)
if match_count:
count_val = match_count.groups()[0].strip().split("=")
if int(count_val[1]) > 999:
errors.append("{0}: {1}".format(_("COUNT > 999 is unsupported"), a_rule))
try:
Schedule.rrulestr(rrule_value)
except Exception as e:
import traceback
logger.error(traceback.format_exc())
raise serializers.ValidationError(_("rrule parsing failed validation: {}").format(e))
errors.append(_("rrule parsing failed validation: {}").format(e))
if errors:
raise serializers.ValidationError(errors)
return value
class ScheduleSerializer(LaunchConfigurationBaseSerializer, SchedulePreviewSerializer):
show_capabilities = ['edit', 'delete']
timezone = serializers.SerializerMethodField()
until = serializers.SerializerMethodField()
timezone = serializers.SerializerMethodField(
help_text=_(
'The timezone this schedule runs in. This field is extracted from the RRULE. If the timezone in the RRULE is a link to another timezone, the link will be reflected in this field.'
),
)
until = serializers.SerializerMethodField(
help_text=_('The date this schedule will end. This field is computed from the RRULE. If the schedule does not end an emptry string will be returned'),
)
class Meta:
model = Schedule
@@ -4761,13 +4750,6 @@ class ScheduleSerializer(LaunchConfigurationBaseSerializer, SchedulePreviewSeria
raise serializers.ValidationError(_('Inventory Source must be a cloud resource.'))
elif type(value) == Project and value.scm_type == '':
raise serializers.ValidationError(_('Manual Project cannot have a schedule set.'))
elif type(value) == InventorySource and value.source == 'scm' and value.update_on_project_update:
raise serializers.ValidationError(
_(
'Inventory sources with `update_on_project_update` cannot be scheduled. '
'Schedule its source project `{}` instead.'.format(value.source_project.name)
)
)
return value
def validate(self, attrs):
@@ -5036,8 +5018,7 @@ class ActivityStreamSerializer(BaseSerializer):
object_association = serializers.SerializerMethodField(help_text=_("When present, shows the field name of the role or relationship that changed."))
object_type = serializers.SerializerMethodField(help_text=_("When present, shows the model on which the role or relationship was defined."))
@cached_property
def _local_summarizable_fk_fields(self):
def _local_summarizable_fk_fields(self, obj):
summary_dict = copy.copy(SUMMARIZABLE_FK_FIELDS)
# Special requests
summary_dict['group'] = summary_dict['group'] + ('inventory_id',)
@@ -5057,7 +5038,13 @@ class ActivityStreamSerializer(BaseSerializer):
('workflow_approval', ('id', 'name', 'unified_job_id')),
('instance', ('id', 'hostname')),
]
return field_list
# Optimization - do not attempt to summarize all fields, pair down to only relations that exist
if not obj:
return field_list
existing_association_types = [obj.object1, obj.object2]
if 'user' in existing_association_types:
existing_association_types.append('role')
return [entry for entry in field_list if entry[0] in existing_association_types]
class Meta:
model = ActivityStream
@@ -5141,7 +5128,7 @@ class ActivityStreamSerializer(BaseSerializer):
data = {}
if obj.actor is not None:
data['actor'] = self.reverse('api:user_detail', kwargs={'pk': obj.actor.pk})
for fk, __ in self._local_summarizable_fk_fields:
for fk, __ in self._local_summarizable_fk_fields(obj):
if not hasattr(obj, fk):
continue
m2m_list = self._get_related_objects(obj, fk)
@@ -5198,7 +5185,7 @@ class ActivityStreamSerializer(BaseSerializer):
def get_summary_fields(self, obj):
summary_fields = OrderedDict()
for fk, related_fields in self._local_summarizable_fk_fields:
for fk, related_fields in self._local_summarizable_fk_fields(obj):
try:
if not hasattr(obj, fk):
continue

17
awx/api/urls/debug.py Normal file
View File

@@ -0,0 +1,17 @@
from django.urls import re_path
from awx.api.views.debug import (
DebugRootView,
TaskManagerDebugView,
DependencyManagerDebugView,
WorkflowManagerDebugView,
)
urls = [
re_path(r'^$', DebugRootView.as_view(), name='debug'),
re_path(r'^task_manager/$', TaskManagerDebugView.as_view(), name='task_manager'),
re_path(r'^dependency_manager/$', DependencyManagerDebugView.as_view(), name='dependency_manager'),
re_path(r'^workflow_manager/$', WorkflowManagerDebugView.as_view(), name='workflow_manager'),
]
__all__ = ['urls']

View File

@@ -2,9 +2,9 @@
# All Rights Reserved.
from __future__ import absolute_import, unicode_literals
from django.conf import settings
from django.urls import include, re_path
from awx import MODE
from awx.api.generics import LoggedLoginView, LoggedLogoutView
from awx.api.views import (
ApiRootView,
@@ -145,7 +145,12 @@ urlpatterns = [
re_path(r'^logout/$', LoggedLogoutView.as_view(next_page='/api/', redirect_field_name='next'), name='logout'),
re_path(r'^o/', include(oauth2_root_urls)),
]
if settings.SETTINGS_MODULE == 'awx.settings.development':
if MODE == 'development':
# Only include these if we are in the development environment
from awx.api.swagger import SwaggerSchemaView
urlpatterns += [re_path(r'^swagger/$', SwaggerSchemaView.as_view(), name='swagger_view')]
from awx.api.urls.debug import urls as debug_urls
urlpatterns += [re_path(r'^debug/', include(debug_urls))]

View File

@@ -93,7 +93,7 @@ from awx.main.utils import (
get_object_or_400,
getattrd,
get_pk_from_dict,
schedule_task_manager,
ScheduleWorkflowManager,
ignore_inventory_computed_fields,
)
from awx.main.utils.encryption import encrypt_value
@@ -115,7 +115,6 @@ from awx.api.metadata import RoleMetadata
from awx.main.constants import ACTIVE_STATES, SURVEY_TYPE_MAPPING
from awx.main.scheduler.dag_workflow import WorkflowDAG
from awx.api.views.mixin import (
ControlledByScmMixin,
InstanceGroupMembershipMixin,
OrganizationCountsMixin,
RelatedJobsPreventDeleteMixin,
@@ -537,6 +536,7 @@ class ScheduleList(ListCreateAPIView):
name = _("Schedules")
model = models.Schedule
serializer_class = serializers.ScheduleSerializer
ordering = ('id',)
class ScheduleDetail(RetrieveUpdateDestroyAPIView):
@@ -577,8 +577,7 @@ class ScheduleZoneInfo(APIView):
swagger_topic = 'System Configuration'
def get(self, request):
zones = [{'name': zone} for zone in models.Schedule.get_zoneinfo()]
return Response(zones)
return Response({'zones': models.Schedule.get_zoneinfo(), 'links': models.Schedule.get_zoneinfo_links()})
class LaunchConfigCredentialsBase(SubListAttachDetachAPIView):
@@ -1675,7 +1674,7 @@ class HostList(HostRelatedSearchMixin, ListCreateAPIView):
return Response(dict(error=_(str(e))), status=status.HTTP_400_BAD_REQUEST)
class HostDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView):
class HostDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
always_allow_superuser = False
model = models.Host
@@ -1709,7 +1708,7 @@ class InventoryHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIVie
return qs
class HostGroupsList(ControlledByScmMixin, SubListCreateAttachDetachAPIView):
class HostGroupsList(SubListCreateAttachDetachAPIView):
'''the list of groups a host is directly a member of'''
model = models.Group
@@ -1825,7 +1824,7 @@ class EnforceParentRelationshipMixin(object):
return super(EnforceParentRelationshipMixin, self).create(request, *args, **kwargs)
class GroupChildrenList(ControlledByScmMixin, EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView):
class GroupChildrenList(EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView):
model = models.Group
serializer_class = serializers.GroupSerializer
@@ -1871,7 +1870,7 @@ class GroupPotentialChildrenList(SubListAPIView):
return qs.exclude(pk__in=except_pks)
class GroupHostsList(HostRelatedSearchMixin, ControlledByScmMixin, SubListCreateAttachDetachAPIView):
class GroupHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIView):
'''the list of hosts directly below a group'''
model = models.Host
@@ -1935,7 +1934,7 @@ class GroupActivityStreamList(SubListAPIView):
return qs.filter(Q(group=parent) | Q(host__in=parent.hosts.all()))
class GroupDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView):
class GroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
model = models.Group
serializer_class = serializers.GroupSerializer
@@ -3392,7 +3391,7 @@ class WorkflowJobCancel(RetrieveAPIView):
obj = self.get_object()
if obj.can_cancel:
obj.cancel()
schedule_task_manager()
ScheduleWorkflowManager().schedule()
return Response(status=status.HTTP_202_ACCEPTED)
else:
return self.http_method_not_allowed(request, *args, **kwargs)
@@ -3840,7 +3839,7 @@ class JobJobEventsList(BaseJobEventsList):
def get_queryset(self):
job = self.get_parent_object()
self.check_parent_access(job)
return job.get_event_queryset().select_related('host').order_by('start_line')
return job.get_event_queryset().prefetch_related('job__job_template', 'host').order_by('start_line')
class JobJobEventsChildrenSummary(APIView):
@@ -3849,7 +3848,7 @@ class JobJobEventsChildrenSummary(APIView):
meta_events = ('debug', 'verbose', 'warning', 'error', 'system_warning', 'deprecated')
def get(self, request, **kwargs):
resp = dict(children_summary={}, meta_event_nested_uuid={}, event_processing_finished=False)
resp = dict(children_summary={}, meta_event_nested_uuid={}, event_processing_finished=False, is_tree=True)
job = get_object_or_404(models.Job, pk=kwargs['pk'])
if not job.event_processing_finished:
return Response(resp)
@@ -3869,13 +3868,41 @@ class JobJobEventsChildrenSummary(APIView):
# key is counter of meta events (i.e. verbose), value is uuid of the assigned parent
map_meta_counter_nested_uuid = {}
# collapsable tree view in the UI only makes sense for tree-like
# hierarchy. If ansible is ran with a strategy like free or host_pinned, then
# events can be out of sequential order, and no longer follow a tree structure
# E1
# E2
# E3
# E4 <- parent is E3
# E5 <- parent is E1
# in the above, there is no clear way to collapse E1, because E5 comes after
# E3, which occurs after E1. Thus the tree view should be disabled.
# mark the last seen uuid at a given level (0-3)
# if a parent uuid is not in this list, then we know the events are not tree-like
# and return a response with is_tree: False
level_current_uuid = [None, None, None, None]
prev_non_meta_event = events[0]
for i, e in enumerate(events):
if not e['event'] in JobJobEventsChildrenSummary.meta_events:
prev_non_meta_event = e
if not e['uuid']:
continue
if not e['event'] in JobJobEventsChildrenSummary.meta_events:
level = models.JobEvent.LEVEL_FOR_EVENT[e['event']]
level_current_uuid[level] = e['uuid']
# if setting level 1, for example, set levels 2 and 3 back to None
for u in range(level + 1, len(level_current_uuid)):
level_current_uuid[u] = None
puuid = e['parent_uuid']
if puuid and puuid not in level_current_uuid:
# improper tree detected, so bail out early
resp['is_tree'] = False
return Response(resp)
# if event is verbose (or debug, etc), we need to "assign" it a
# parent. This code looks at the event level of the previous

68
awx/api/views/debug.py Normal file
View File

@@ -0,0 +1,68 @@
from collections import OrderedDict
from django.conf import settings
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from awx.api.generics import APIView
from awx.main.scheduler import TaskManager, DependencyManager, WorkflowManager
class TaskManagerDebugView(APIView):
_ignore_model_permissions = True
exclude_from_schema = True
permission_classes = [AllowAny]
prefix = 'Task'
def get(self, request):
TaskManager().schedule()
if not settings.AWX_DISABLE_TASK_MANAGERS:
msg = f"Running {self.prefix} manager. To disable other triggers to the {self.prefix} manager, set AWX_DISABLE_TASK_MANAGERS to True"
else:
msg = f"AWX_DISABLE_TASK_MANAGERS is True, this view is the only way to trigger the {self.prefix} manager"
return Response(msg)
class DependencyManagerDebugView(APIView):
_ignore_model_permissions = True
exclude_from_schema = True
permission_classes = [AllowAny]
prefix = 'Dependency'
def get(self, request):
DependencyManager().schedule()
if not settings.AWX_DISABLE_TASK_MANAGERS:
msg = f"Running {self.prefix} manager. To disable other triggers to the {self.prefix} manager, set AWX_DISABLE_TASK_MANAGERS to True"
else:
msg = f"AWX_DISABLE_TASK_MANAGERS is True, this view is the only way to trigger the {self.prefix} manager"
return Response(msg)
class WorkflowManagerDebugView(APIView):
_ignore_model_permissions = True
exclude_from_schema = True
permission_classes = [AllowAny]
prefix = 'Workflow'
def get(self, request):
WorkflowManager().schedule()
if not settings.AWX_DISABLE_TASK_MANAGERS:
msg = f"Running {self.prefix} manager. To disable other triggers to the {self.prefix} manager, set AWX_DISABLE_TASK_MANAGERS to True"
else:
msg = f"AWX_DISABLE_TASK_MANAGERS is True, this view is the only way to trigger the {self.prefix} manager"
return Response(msg)
class DebugRootView(APIView):
_ignore_model_permissions = True
exclude_from_schema = True
permission_classes = [AllowAny]
def get(self, request, format=None):
'''List of available debug urls'''
data = OrderedDict()
data['task_manager'] = '/api/debug/task_manager/'
data['dependency_manager'] = '/api/debug/dependency_manager/'
data['workflow_manager'] = '/api/debug/workflow_manager/'
return Response(data)

View File

@@ -41,7 +41,7 @@ from awx.api.serializers import (
JobTemplateSerializer,
LabelSerializer,
)
from awx.api.views.mixin import RelatedJobsPreventDeleteMixin, ControlledByScmMixin
from awx.api.views.mixin import RelatedJobsPreventDeleteMixin
from awx.api.pagination import UnifiedJobEventPagination
@@ -75,7 +75,7 @@ class InventoryList(ListCreateAPIView):
serializer_class = InventorySerializer
class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView):
class InventoryDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
model = Inventory
serializer_class = InventorySerializer

View File

@@ -10,13 +10,12 @@ from django.shortcuts import get_object_or_404
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from rest_framework.permissions import SAFE_METHODS
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework import status
from awx.main.constants import ACTIVE_STATES
from awx.main.utils import get_object_or_400, parse_yaml_or_json
from awx.main.utils import get_object_or_400
from awx.main.models.ha import Instance, InstanceGroup
from awx.main.models.organization import Team
from awx.main.models.projects import Project
@@ -186,35 +185,6 @@ class OrganizationCountsMixin(object):
return full_context
class ControlledByScmMixin(object):
"""
Special method to reset SCM inventory commit hash
if anything that it manages changes.
"""
def _reset_inv_src_rev(self, obj):
if self.request.method in SAFE_METHODS or not obj:
return
project_following_sources = obj.inventory_sources.filter(update_on_project_update=True, source='scm')
if project_following_sources:
# Allow inventory changes unrelated to variables
if self.model == Inventory and (
not self.request or not self.request.data or parse_yaml_or_json(self.request.data.get('variables', '')) == parse_yaml_or_json(obj.variables)
):
return
project_following_sources.update(scm_last_revision='')
def get_object(self):
obj = super(ControlledByScmMixin, self).get_object()
self._reset_inv_src_rev(obj)
return obj
def get_parent_object(self):
obj = super(ControlledByScmMixin, self).get_parent_object()
self._reset_inv_src_rev(obj)
return obj
class NoTruncateMixin(object):
def get_serializer_context(self):
context = super().get_serializer_context()

View File

@@ -204,7 +204,7 @@ class GitlabWebhookReceiver(WebhookReceiverBase):
return h.hexdigest()
def get_event_status_api(self):
if self.get_event_type() != 'Merge Request Hook':
if self.get_event_type() not in self.ref_keys.keys():
return
project = self.request.data.get('project', {})
repo_url = project.get('web_url')

View File

@@ -1,7 +1,6 @@
# Python
import contextlib
import logging
import sys
import threading
import time
import os
@@ -31,7 +30,7 @@ from awx.conf.models import Setting
logger = logging.getLogger('awx.conf.settings')
SETTING_MEMORY_TTL = 5 if 'callback_receiver' in ' '.join(sys.argv) else 0
SETTING_MEMORY_TTL = 5
# Store a special value to indicate when a setting is not set in the database.
SETTING_CACHE_NOTSET = '___notset___'
@@ -81,7 +80,7 @@ def _ctit_db_wrapper(trans_safe=False):
yield
except DBError as exc:
if trans_safe:
level = logger.exception
level = logger.warning
if isinstance(exc, ProgrammingError):
if 'relation' in str(exc) and 'does not exist' in str(exc):
# this generally means we can't fetch Tower configuration
@@ -90,7 +89,7 @@ def _ctit_db_wrapper(trans_safe=False):
# has come up *before* the database has finished migrating, and
# especially that the conf.settings table doesn't exist yet
level = logger.debug
level('Database settings are not available, using defaults.')
level(f'Database settings are not available, using defaults. error: {str(exc)}')
else:
logger.exception('Error modifying something related to database settings.')
finally:
@@ -234,6 +233,8 @@ class SettingsWrapper(UserSettingsHolder):
self.__dict__['_awx_conf_init_readonly'] = False
self.__dict__['cache'] = EncryptedCacheProxy(cache, registry)
self.__dict__['registry'] = registry
self.__dict__['_awx_conf_memoizedcache'] = cachetools.TTLCache(maxsize=2048, ttl=SETTING_MEMORY_TTL)
self.__dict__['_awx_conf_memoizedcache_lock'] = threading.Lock()
# record the current pid so we compare it post-fork for
# processes like the dispatcher and callback receiver
@@ -396,12 +397,20 @@ class SettingsWrapper(UserSettingsHolder):
def SETTINGS_MODULE(self):
return self._get_default('SETTINGS_MODULE')
@cachetools.cached(cache=cachetools.TTLCache(maxsize=2048, ttl=SETTING_MEMORY_TTL))
@cachetools.cachedmethod(
cache=lambda self: self.__dict__['_awx_conf_memoizedcache'],
key=lambda *args, **kwargs: SettingsWrapper.hashkey(*args, **kwargs),
lock=lambda self: self.__dict__['_awx_conf_memoizedcache_lock'],
)
def _get_local_with_cache(self, name):
"""Get value while accepting the in-memory cache if key is available"""
with _ctit_db_wrapper(trans_safe=True):
return self._get_local(name)
def __getattr__(self, name):
value = empty
if name in self.all_supported_settings:
with _ctit_db_wrapper(trans_safe=True):
value = self._get_local(name)
value = self._get_local_with_cache(name)
if value is not empty:
return value
return self._get_default(name)
@@ -475,6 +484,23 @@ class SettingsWrapper(UserSettingsHolder):
set_on_default = getattr(self.default_settings, 'is_overridden', lambda s: False)(setting)
return set_locally or set_on_default
@classmethod
def hashkey(cls, *args, **kwargs):
"""
Usage of @cachetools.cached has changed to @cachetools.cachedmethod
The previous cachetools decorator called the hash function and passed in (self, key).
The new cachtools decorator calls the hash function with just (key).
Ideally, we would continue to pass self, however, the cachetools decorator interface
does not allow us to.
This hashkey function is to maintain that the key generated looks like
('<SettingsWrapper>', key). The thought is that maybe it is important to namespace
our cache to the SettingsWrapper scope in case some other usage of this cache exists.
I can not think of how any other system could and would use our private cache, but
for safety sake we are ensuring the key schema does not change.
"""
return cachetools.keys.hashkey(f"<{cls.__name__}>", *args, **kwargs)
def __getattr_without_cache__(self, name):
# Django 1.10 added an optimization to settings lookup:

View File

@@ -28,6 +28,9 @@ def handle_setting_change(key, for_delete=False):
cache_keys = {Setting.get_cache_key(k) for k in setting_keys}
cache.delete_many(cache_keys)
# if we have changed a setting, we want to avoid mucking with the in-memory cache entirely
settings._awx_conf_memoizedcache.clear()
# Send setting_changed signal with new value for each setting.
for setting_key in setting_keys:
setting_changed.send(sender=Setting, setting=setting_key, value=getattr(settings, setting_key, None), enter=not bool(for_delete))

View File

@@ -8,6 +8,8 @@ import codecs
from uuid import uuid4
import time
from unittest import mock
from django.conf import LazySettings
from django.core.cache.backends.locmem import LocMemCache
from django.core.exceptions import ImproperlyConfigured
@@ -299,3 +301,33 @@ def test_readonly_sensitive_cache_data_is_encrypted(settings):
cache.set('AWX_ENCRYPTED', 'SECRET!')
assert cache.get('AWX_ENCRYPTED') == 'SECRET!'
assert native_cache.get('AWX_ENCRYPTED') == 'FRPERG!'
@pytest.mark.defined_in_file(AWX_VAR='DEFAULT')
def test_in_memory_cache_only_for_registered_settings(settings):
"Test that we only make use of the in-memory TTL cache for registered settings"
settings._awx_conf_memoizedcache.clear()
settings.MIDDLEWARE
assert len(settings._awx_conf_memoizedcache) == 0 # does not cache MIDDLEWARE
settings.registry.register('AWX_VAR', field_class=fields.CharField, category=_('System'), category_slug='system')
settings._wrapped.__dict__['all_supported_settings'] = ['AWX_VAR'] # because it is cached_property
settings._awx_conf_memoizedcache.clear()
assert settings.AWX_VAR == 'DEFAULT'
assert len(settings._awx_conf_memoizedcache) == 1 # caches registered settings
@pytest.mark.defined_in_file(AWX_VAR='DEFAULT')
def test_in_memory_cache_works(settings):
settings._awx_conf_memoizedcache.clear()
settings.registry.register('AWX_VAR', field_class=fields.CharField, category=_('System'), category_slug='system')
settings._wrapped.__dict__['all_supported_settings'] = ['AWX_VAR']
settings._awx_conf_memoizedcache.clear()
with mock.patch('awx.conf.settings.SettingsWrapper._get_local', return_value='DEFAULT') as mock_get:
assert settings.AWX_VAR == 'DEFAULT'
mock_get.assert_called_once_with('AWX_VAR')
with mock.patch.object(settings, '_get_local') as mock_get:
assert settings.AWX_VAR == 'DEFAULT'
mock_get.assert_not_called()

View File

@@ -1440,7 +1440,7 @@ msgstr "指定した認証情報は無効 (HTTP 401) です。"
#: awx/api/views/root.py:193 awx/api/views/root.py:234
msgid "Unable to connect to proxy server."
msgstr "プロキシサーバーに接続できません。"
msgstr "プロキシサーバーに接続できません。"
#: awx/api/views/root.py:195 awx/api/views/root.py:236
msgid "Could not connect to subscription service."
@@ -1976,7 +1976,7 @@ msgstr "リモートホスト名または IP を判別するために検索す
#: awx/main/conf.py:85
msgid "Proxy IP Allowed List"
msgstr "プロキシ IP 許可リスト"
msgstr "プロキシ IP 許可リスト"
#: awx/main/conf.py:87
msgid ""
@@ -2198,7 +2198,7 @@ msgid ""
"Follow symbolic links when scanning for playbooks. Be aware that setting "
"this to True can lead to infinite recursion if a link points to a parent "
"directory of itself."
msgstr "Playbook スキャンするときは、シンボリックリンクをたどってください。リンクがそれ自体の親ディレクトリーをしている場合は、こを True に定すると無限再帰が発生する可能性があることに注意してください。"
msgstr "Playbook スキャン時にシンボリックリンクをたどります。リンクが親ディレクトリーを参照している場合は、この設定を True に定すると無限再帰が発生する可能性があります。"
#: awx/main/conf.py:337
msgid "Ignore Ansible Galaxy SSL Certificate Verification"
@@ -2499,7 +2499,7 @@ msgstr "Insights for Ansible Automation Platform の最終収集日。"
msgid ""
"Last gathered entries for expensive collectors for Insights for Ansible "
"Automation Platform."
msgstr "Insights for Ansible Automation Platform の高価なコレクター最後に収集されたエントリー"
msgstr "Insights for Ansible Automation Platform でコストがかかっているコレクターに関して最後に収集されたエントリー"
#: awx/main/conf.py:686
msgid "Insights for Ansible Automation Platform Gather Interval"
@@ -3692,7 +3692,7 @@ msgstr "タスクの開始"
#: awx/main/models/events.py:189
msgid "Variables Prompted"
msgstr "変数のプロモート"
msgstr "提示される変数"
#: awx/main/models/events.py:190
msgid "Gathering Facts"
@@ -3741,15 +3741,15 @@ msgstr "エラー"
#: awx/main/models/execution_environments.py:17
msgid "Always pull container before running."
msgstr "実行前に必ずコンテナーをプルしてください。"
msgstr "実行前に必ずコンテナーをプルする"
#: awx/main/models/execution_environments.py:18
msgid "Only pull the image if not present before running."
msgstr "実行する前に、存在しない場合のみイメージをプルしてください。"
msgstr "イメージが存在しない場合のみ実行前にプルする"
#: awx/main/models/execution_environments.py:19
msgid "Never pull container before running."
msgstr "実行前にコンテナーをプルしないでください。"
msgstr "実行前にコンテナーをプルしない"
#: awx/main/models/execution_environments.py:29
msgid ""
@@ -5228,7 +5228,7 @@ msgid ""
"SSL) or \"ldaps://ldap.example.com:636\" (SSL). Multiple LDAP servers may be "
"specified by separating with spaces or commas. LDAP authentication is "
"disabled if this parameter is empty."
msgstr "\"ldap://ldap.example.com:389\" (非 SSL) または \"ldaps://ldap.example.com:636\" (SSL) などの LDAP サーバーに接続する URI です。複数の LDAP サーバーをスペースまたはンマで区切って指定できます。LDAP 認証は、このパラメーターが空の場合は無効になります。"
msgstr "\"ldap://ldap.example.com:389\" (非 SSL) または \"ldaps://ldap.example.com:636\" (SSL) などの LDAP サーバーに接続する URI です。複数の LDAP サーバーをスペースまたはンマで区切って指定できます。LDAP 認証は、このパラメーターが空の場合は無効になります。"
#: awx/sso/conf.py:170 awx/sso/conf.py:187 awx/sso/conf.py:198
#: awx/sso/conf.py:209 awx/sso/conf.py:226 awx/sso/conf.py:244
@@ -6236,4 +6236,5 @@ msgstr "%s が現在アップグレード中です。"
#: awx/ui/urls.py:24
msgid "This page will refresh when complete."
msgstr "このページは完了すると更新されます。"
msgstr "このページは完了すると更新されます。"

View File

@@ -956,7 +956,7 @@ msgstr "인스턴스 그룹의 인스턴스"
#: awx/api/views/__init__.py:450
msgid "Schedules"
msgstr "일정"
msgstr "스케줄"
#: awx/api/views/__init__.py:464
msgid "Schedule Recurrence Rule Preview"
@@ -3261,7 +3261,7 @@ msgstr "JSON 또는 YAML 구문을 사용하여 인젝터를 입력합니다.
#: awx/main/models/credential/__init__.py:412
#, python-format
msgid "adding %s credential type"
msgstr "인증 정보 유형 %s 추가 중"
msgstr "인증 정보 유형 %s 추가 중"
#: awx/main/models/credential/__init__.py:590
#: awx/main/models/credential/__init__.py:672
@@ -6236,4 +6236,5 @@ msgstr "%s 현재 업그레이드 중입니다."
#: awx/ui/urls.py:24
msgid "This page will refresh when complete."
msgstr "완료되면 이 페이지가 새로 고침됩니다."
msgstr "완료되면 이 페이지가 새로 고침됩니다."

View File

@@ -348,7 +348,7 @@ msgstr "SCM track_submodules 只能用于 git 项目。"
msgid ""
"Only Container Registry credentials can be associated with an Execution "
"Environment"
msgstr "只有容器 registry 凭证可以与执行环境关联"
msgstr "只有容器注册表凭证可以与执行环境关联"
#: awx/api/serializers.py:1440
msgid "Cannot change the organization of an execution environment"
@@ -629,7 +629,7 @@ msgstr "不支持在不替换的情况下在启动时删除 {} 凭证。提供
#: awx/api/serializers.py:4338
msgid "The inventory associated with this Workflow is being deleted."
msgstr "与此 Workflow 关联的清单将被删除。"
msgstr "与此工作流关联的清单将被删除。"
#: awx/api/serializers.py:4405
msgid "Message type '{}' invalid, must be either 'message' or 'body'"
@@ -3229,7 +3229,7 @@ msgstr "云"
#: awx/main/models/credential/__init__.py:336
#: awx/main/models/credential/__init__.py:1113
msgid "Container Registry"
msgstr "容器 Registry"
msgstr "容器注册表"
#: awx/main/models/credential/__init__.py:337
msgid "Personal Access Token"
@@ -3560,7 +3560,7 @@ msgstr "身份验证 URL"
#: awx/main/models/credential/__init__.py:1120
msgid "Authentication endpoint for the container registry."
msgstr "容器 registry 的身份验证端点。"
msgstr "容器注册表的身份验证端点。"
#: awx/main/models/credential/__init__.py:1130
msgid "Password or Token"
@@ -3764,7 +3764,7 @@ msgstr "镜像位置"
msgid ""
"The full image location, including the container registry, image name, and "
"version tag."
msgstr "完整镜像位置,包括容器 registry、镜像名称和版本标签。"
msgstr "完整镜像位置,包括容器注册表、镜像名称和版本标签。"
#: awx/main/models/execution_environments.py:51
msgid "Pull image before running?"
@@ -6238,4 +6238,5 @@ msgstr "%s 当前正在升级。"
#: awx/ui/urls.py:24
msgid "This page will refresh when complete."
msgstr "完成后,此页面会刷新。"
msgstr "完成后,此页面会刷新。"

View File

@@ -12,12 +12,11 @@ from django.contrib.sessions.models import Session
from django.utils.timezone import now, timedelta
from django.utils.translation import gettext_lazy as _
from psycopg2.errors import UntranslatableCharacter
from awx.conf.license import get_license
from awx.main.utils import get_awx_version, camelcase_to_underscore, datetime_hook
from awx.main import models
from awx.main.analytics import register
from awx.main.scheduler.task_manager_models import TaskManagerInstances
"""
This module is used to define metrics collected by awx.main.analytics.gather()
@@ -131,7 +130,7 @@ def config(since, **kwargs):
}
@register('counts', '1.1', description=_('Counts of objects such as organizations, inventories, and projects'))
@register('counts', '1.2', description=_('Counts of objects such as organizations, inventories, and projects'))
def counts(since, **kwargs):
counts = {}
for cls in (
@@ -174,6 +173,13 @@ def counts(since, **kwargs):
.count()
)
counts['pending_jobs'] = models.UnifiedJob.objects.exclude(launch_type='sync').filter(status__in=('pending',)).count()
if connection.vendor == 'postgresql':
with connection.cursor() as cursor:
cursor.execute(f"select count(*) from pg_stat_activity where datname=\'{connection.settings_dict['NAME']}\'")
counts['database_connections'] = cursor.fetchone()[0]
else:
# We should be using postgresql, but if we do that change that ever we should change the below value
counts['database_connections'] = 1
return counts
@@ -230,25 +236,25 @@ def projects_by_scm_type(since, **kwargs):
@register('instance_info', '1.2', description=_('Cluster topology and capacity'))
def instance_info(since, include_hostnames=False, **kwargs):
info = {}
instances = models.Instance.objects.values_list('hostname').values(
'uuid', 'version', 'capacity', 'cpu', 'memory', 'managed_by_policy', 'hostname', 'enabled'
)
for instance in instances:
consumed_capacity = sum(x.task_impact for x in models.UnifiedJob.objects.filter(execution_node=instance['hostname'], status__in=('running', 'waiting')))
# Use same method that the TaskManager does to compute consumed capacity without querying all running jobs for each Instance
active_tasks = models.UnifiedJob.objects.filter(status__in=['running', 'waiting']).only('task_impact', 'controller_node', 'execution_node')
tm_instances = TaskManagerInstances(active_tasks, instance_fields=['uuid', 'version', 'capacity', 'cpu', 'memory', 'managed_by_policy', 'enabled'])
for tm_instance in tm_instances.instances_by_hostname.values():
instance = tm_instance.obj
instance_info = {
'uuid': instance['uuid'],
'version': instance['version'],
'capacity': instance['capacity'],
'cpu': instance['cpu'],
'memory': instance['memory'],
'managed_by_policy': instance['managed_by_policy'],
'enabled': instance['enabled'],
'consumed_capacity': consumed_capacity,
'remaining_capacity': instance['capacity'] - consumed_capacity,
'uuid': instance.uuid,
'version': instance.version,
'capacity': instance.capacity,
'cpu': instance.cpu,
'memory': instance.memory,
'managed_by_policy': instance.managed_by_policy,
'enabled': instance.enabled,
'consumed_capacity': tm_instance.consumed_capacity,
'remaining_capacity': instance.capacity - tm_instance.consumed_capacity,
}
if include_hostnames is True:
instance_info['hostname'] = instance['hostname']
info[instance['uuid']] = instance_info
instance_info['hostname'] = instance.hostname
info[instance.uuid] = instance_info
return info
@@ -378,10 +384,7 @@ def _events_table(since, full_path, until, tbl, where_column, project_job_create
WHERE ({tbl}.{where_column} > '{since.isoformat()}' AND {tbl}.{where_column} <= '{until.isoformat()}')) TO STDOUT WITH CSV HEADER'''
return query
try:
return _copy_table(table='events', query=query(f"{tbl}.event_data::jsonb"), path=full_path)
except UntranslatableCharacter:
return _copy_table(table='events', query=query(f"replace({tbl}.event_data::text, '\\u0000', '')::jsonb"), path=full_path)
return _copy_table(table='events', query=query(fr"replace({tbl}.event_data, '\u', '\u005cu')::jsonb"), path=full_path)
@register('events_table', '1.5', format='csv', description=_('Automation task records'), expensive=four_hour_slicing)
@@ -394,7 +397,7 @@ def events_table_partitioned_modified(since, full_path, until, **kwargs):
return _events_table(since, full_path, until, 'main_jobevent', 'modified', project_job_created=True, **kwargs)
@register('unified_jobs_table', '1.3', format='csv', description=_('Data on jobs run'), expensive=four_hour_slicing)
@register('unified_jobs_table', '1.4', format='csv', description=_('Data on jobs run'), expensive=four_hour_slicing)
def unified_jobs_table(since, full_path, until, **kwargs):
unified_job_query = '''COPY (SELECT main_unifiedjob.id,
main_unifiedjob.polymorphic_ctype_id,
@@ -420,7 +423,8 @@ def unified_jobs_table(since, full_path, until, **kwargs):
main_unifiedjob.job_explanation,
main_unifiedjob.instance_group_id,
main_unifiedjob.installed_collections,
main_unifiedjob.ansible_version
main_unifiedjob.ansible_version,
main_job.forks
FROM main_unifiedjob
JOIN django_content_type ON main_unifiedjob.polymorphic_ctype_id = django_content_type.id
LEFT JOIN main_job ON main_unifiedjob.id = main_job.unifiedjob_ptr_id

View File

@@ -126,6 +126,8 @@ def metrics():
LICENSE_INSTANCE_TOTAL = Gauge('awx_license_instance_total', 'Total number of managed hosts provided by your license', registry=REGISTRY)
LICENSE_INSTANCE_FREE = Gauge('awx_license_instance_free', 'Number of remaining managed hosts provided by your license', registry=REGISTRY)
DATABASE_CONNECTIONS = Gauge('awx_database_connections_total', 'Number of connections to database', registry=REGISTRY)
license_info = get_license()
SYSTEM_INFO.info(
{
@@ -163,6 +165,8 @@ def metrics():
USER_SESSIONS.labels(type='user').set(current_counts['active_user_sessions'])
USER_SESSIONS.labels(type='anonymous').set(current_counts['active_anonymous_sessions'])
DATABASE_CONNECTIONS.set(current_counts['database_connections'])
all_job_data = job_counts(None)
statuses = all_job_data.get('status', {})
for status, value in statuses.items():

View File

@@ -8,7 +8,7 @@ from django.apps import apps
from awx.main.consumers import emit_channel_notification
root_key = 'awx_metrics'
logger = logging.getLogger('awx.main.wsbroadcast')
logger = logging.getLogger('awx.main.analytics')
class BaseM:
@@ -16,16 +16,22 @@ class BaseM:
self.field = field
self.help_text = help_text
self.current_value = 0
self.metric_has_changed = False
def clear_value(self, conn):
def reset_value(self, conn):
conn.hset(root_key, self.field, 0)
self.current_value = 0
def inc(self, value):
self.current_value += value
self.metric_has_changed = True
def set(self, value):
self.current_value = value
self.metric_has_changed = True
def get(self):
return self.current_value
def decode(self, conn):
value = conn.hget(root_key, self.field)
@@ -34,7 +40,9 @@ class BaseM:
def to_prometheus(self, instance_data):
output_text = f"# HELP {self.field} {self.help_text}\n# TYPE {self.field} gauge\n"
for instance in instance_data:
output_text += f'{self.field}{{node="{instance}"}} {instance_data[instance][self.field]}\n'
if self.field in instance_data[instance]:
# on upgrade, if there are stale instances, we can end up with issues where new metrics are not present
output_text += f'{self.field}{{node="{instance}"}} {instance_data[instance][self.field]}\n'
return output_text
@@ -46,8 +54,10 @@ class FloatM(BaseM):
return 0.0
def store_value(self, conn):
conn.hincrbyfloat(root_key, self.field, self.current_value)
self.current_value = 0
if self.metric_has_changed:
conn.hincrbyfloat(root_key, self.field, self.current_value)
self.current_value = 0
self.metric_has_changed = False
class IntM(BaseM):
@@ -58,8 +68,10 @@ class IntM(BaseM):
return 0
def store_value(self, conn):
conn.hincrby(root_key, self.field, self.current_value)
self.current_value = 0
if self.metric_has_changed:
conn.hincrby(root_key, self.field, self.current_value)
self.current_value = 0
self.metric_has_changed = False
class SetIntM(BaseM):
@@ -70,10 +82,9 @@ class SetIntM(BaseM):
return 0
def store_value(self, conn):
# do not set value if it has not changed since last time this was called
if self.current_value is not None:
if self.metric_has_changed:
conn.hset(root_key, self.field, self.current_value)
self.current_value = None
self.metric_has_changed = False
class SetFloatM(SetIntM):
@@ -94,13 +105,13 @@ class HistogramM(BaseM):
self.sum = IntM(field + '_sum', '')
super(HistogramM, self).__init__(field, help_text)
def clear_value(self, conn):
def reset_value(self, conn):
conn.hset(root_key, self.field, 0)
self.inf.clear_value(conn)
self.sum.clear_value(conn)
self.inf.reset_value(conn)
self.sum.reset_value(conn)
for b in self.buckets_to_keys.values():
b.clear_value(conn)
super(HistogramM, self).clear_value(conn)
b.reset_value(conn)
super(HistogramM, self).reset_value(conn)
def observe(self, value):
for b in self.buckets:
@@ -136,7 +147,7 @@ class HistogramM(BaseM):
class Metrics:
def __init__(self, auto_pipe_execute=True, instance_name=None):
def __init__(self, auto_pipe_execute=False, instance_name=None):
self.pipe = redis.Redis.from_url(settings.BROKER_URL).pipeline()
self.conn = redis.Redis.from_url(settings.BROKER_URL)
self.last_pipe_execute = time.time()
@@ -152,8 +163,14 @@ class Metrics:
Instance = apps.get_model('main', 'Instance')
if instance_name:
self.instance_name = instance_name
elif settings.IS_TESTING():
self.instance_name = "awx_testing"
else:
self.instance_name = Instance.objects.me().hostname
try:
self.instance_name = Instance.objects.me().hostname
except Exception as e:
self.instance_name = settings.CLUSTER_HOST_ID
logger.info(f'Instance {self.instance_name} seems to be unregistered, error: {e}')
# metric name, help_text
METRICSLIST = [
@@ -161,15 +178,39 @@ class Metrics:
IntM('callback_receiver_events_popped_redis', 'Number of events popped from redis'),
IntM('callback_receiver_events_in_memory', 'Current number of events in memory (in transfer from redis to db)'),
IntM('callback_receiver_batch_events_errors', 'Number of times batch insertion failed'),
FloatM('callback_receiver_events_insert_db_seconds', 'Time spent saving events to database'),
FloatM('callback_receiver_events_insert_db_seconds', 'Total time spent saving events to database'),
IntM('callback_receiver_events_insert_db', 'Number of events batch inserted into database'),
IntM('callback_receiver_events_broadcast', 'Number of events broadcast to other control plane nodes'),
HistogramM(
'callback_receiver_batch_events_insert_db', 'Number of events batch inserted into database', settings.SUBSYSTEM_METRICS_BATCH_INSERT_BUCKETS
),
SetFloatM('callback_receiver_event_processing_avg_seconds', 'Average processing time per event per callback receiver batch'),
FloatM('subsystem_metrics_pipe_execute_seconds', 'Time spent saving metrics to redis'),
IntM('subsystem_metrics_pipe_execute_calls', 'Number of calls to pipe_execute'),
FloatM('subsystem_metrics_send_metrics_seconds', 'Time spent sending metrics to other nodes'),
SetFloatM('task_manager_get_tasks_seconds', 'Time spent in loading tasks from db'),
SetFloatM('task_manager_start_task_seconds', 'Time spent starting task'),
SetFloatM('task_manager_process_running_tasks_seconds', 'Time spent processing running tasks'),
SetFloatM('task_manager_process_pending_tasks_seconds', 'Time spent processing pending tasks'),
SetFloatM('task_manager__schedule_seconds', 'Time spent in running the entire _schedule'),
IntM('task_manager__schedule_calls', 'Number of calls to _schedule, after lock is acquired'),
SetFloatM('task_manager_recorded_timestamp', 'Unix timestamp when metrics were last recorded'),
SetIntM('task_manager_tasks_started', 'Number of tasks started'),
SetIntM('task_manager_running_processed', 'Number of running tasks processed'),
SetIntM('task_manager_pending_processed', 'Number of pending tasks processed'),
SetIntM('task_manager_tasks_blocked', 'Number of tasks blocked from running'),
SetFloatM('task_manager_commit_seconds', 'Time spent in db transaction, including on_commit calls'),
SetFloatM('dependency_manager_get_tasks_seconds', 'Time spent loading pending tasks from db'),
SetFloatM('dependency_manager_generate_dependencies_seconds', 'Time spent generating dependencies for pending tasks'),
SetFloatM('dependency_manager__schedule_seconds', 'Time spent in running the entire _schedule'),
IntM('dependency_manager__schedule_calls', 'Number of calls to _schedule, after lock is acquired'),
SetFloatM('dependency_manager_recorded_timestamp', 'Unix timestamp when metrics were last recorded'),
SetIntM('dependency_manager_pending_processed', 'Number of pending tasks processed'),
SetFloatM('workflow_manager__schedule_seconds', 'Time spent in running the entire _schedule'),
IntM('workflow_manager__schedule_calls', 'Number of calls to _schedule, after lock is acquired'),
SetFloatM('workflow_manager_recorded_timestamp', 'Unix timestamp when metrics were last recorded'),
SetFloatM('workflow_manager_spawn_workflow_graph_jobs_seconds', 'Time spent spawning workflow tasks'),
SetFloatM('workflow_manager_get_tasks_seconds', 'Time spent loading workflow tasks from db'),
]
# turn metric list into dictionary with the metric name as a key
self.METRICS = {}
@@ -179,29 +220,39 @@ class Metrics:
# track last time metrics were sent to other nodes
self.previous_send_metrics = SetFloatM('send_metrics_time', 'Timestamp of previous send_metrics call')
def clear_values(self):
def reset_values(self):
# intended to be called once on app startup to reset all metric
# values to 0
for m in self.METRICS.values():
m.clear_value(self.conn)
m.reset_value(self.conn)
self.metrics_have_changed = True
self.conn.delete(root_key + "_lock")
for m in self.conn.scan_iter(root_key + '_instance_*'):
self.conn.delete(m)
def inc(self, field, value):
if value != 0:
self.METRICS[field].inc(value)
self.metrics_have_changed = True
if self.auto_pipe_execute is True and self.should_pipe_execute() is True:
if self.auto_pipe_execute is True:
self.pipe_execute()
def set(self, field, value):
self.METRICS[field].set(value)
self.metrics_have_changed = True
if self.auto_pipe_execute is True and self.should_pipe_execute() is True:
if self.auto_pipe_execute is True:
self.pipe_execute()
def get(self, field):
return self.METRICS[field].get()
def decode(self, field):
return self.METRICS[field].decode(self.conn)
def observe(self, field, value):
self.METRICS[field].observe(value)
self.metrics_have_changed = True
if self.auto_pipe_execute is True and self.should_pipe_execute() is True:
if self.auto_pipe_execute is True:
self.pipe_execute()
def serialize_local_metrics(self):
@@ -249,8 +300,8 @@ class Metrics:
def send_metrics(self):
# more than one thread could be calling this at the same time, so should
# get acquire redis lock before sending metrics
lock = self.conn.lock(root_key + '_lock', thread_local=False)
# acquire redis lock before sending metrics
lock = self.conn.lock(root_key + '_lock')
if not lock.acquire(blocking=False):
return
try:
@@ -266,7 +317,12 @@ class Metrics:
self.previous_send_metrics.set(current_time)
self.previous_send_metrics.store_value(self.conn)
finally:
lock.release()
try:
lock.release()
except Exception as exc:
# After system failures, we might throw redis.exceptions.LockNotOwnedError
# this is to avoid print a Traceback, and importantly, avoid raising an exception into parent context
logger.warning(f'Error releasing subsystem metrics redis lock, error: {str(exc)}')
def load_other_metrics(self, request):
# data received from other nodes are stored in their own keys

View File

@@ -446,7 +446,7 @@ register(
label=_('Default Job Idle Timeout'),
help_text=_(
'If no output is detected from ansible in this number of seconds the execution will be terminated. '
'Use value of 0 to used default idle_timeout is 600s.'
'Use value of 0 to indicate that no idle timeout should be imposed.'
),
category=_('Jobs'),
category_slug='jobs',

View File

@@ -4,6 +4,7 @@ import select
from contextlib import contextmanager
from django.conf import settings
from django.db import connection as pg_connection
NOT_READY = ([], [], [])
@@ -15,7 +16,6 @@ def get_local_queuename():
class PubSub(object):
def __init__(self, conn):
assert conn.autocommit, "Connection must be in autocommit mode."
self.conn = conn
def listen(self, channel):
@@ -31,6 +31,9 @@ class PubSub(object):
cur.execute('SELECT pg_notify(%s, %s);', (channel, payload))
def events(self, select_timeout=5, yield_timeouts=False):
if not self.conn.autocommit:
raise RuntimeError('Listening for events can only be done in autocommit mode')
while True:
if select.select([self.conn], [], [], select_timeout) == NOT_READY:
if yield_timeouts:
@@ -45,11 +48,32 @@ class PubSub(object):
@contextmanager
def pg_bus_conn():
conf = settings.DATABASES['default']
conn = psycopg2.connect(dbname=conf['NAME'], host=conf['HOST'], user=conf['USER'], password=conf['PASSWORD'], port=conf['PORT'], **conf.get("OPTIONS", {}))
# Django connection.cursor().connection doesn't have autocommit=True on
conn.set_session(autocommit=True)
def pg_bus_conn(new_connection=False):
'''
Any listeners probably want to establish a new database connection,
separate from the Django connection used for queries, because that will prevent
losing connection to the channel whenever a .close() happens.
Any publishers probably want to use the existing connection
so that messages follow postgres transaction rules
https://www.postgresql.org/docs/current/sql-notify.html
'''
if new_connection:
conf = settings.DATABASES['default']
conn = psycopg2.connect(
dbname=conf['NAME'], host=conf['HOST'], user=conf['USER'], password=conf['PASSWORD'], port=conf['PORT'], **conf.get("OPTIONS", {})
)
# Django connection.cursor().connection doesn't have autocommit=True on by default
conn.set_session(autocommit=True)
else:
if pg_connection.connection is None:
pg_connection.connect()
if pg_connection.connection is None:
raise RuntimeError('Unexpectedly could not connect to postgres for pg_notify actions')
conn = pg_connection.connection
pubsub = PubSub(conn)
yield pubsub
conn.close()
if new_connection:
conn.close()

View File

@@ -37,18 +37,24 @@ class Control(object):
def running(self, *args, **kwargs):
return self.control_with_reply('running', *args, **kwargs)
def cancel(self, task_ids, *args, **kwargs):
return self.control_with_reply('cancel', *args, extra_data={'task_ids': task_ids}, **kwargs)
@classmethod
def generate_reply_queue_name(cls):
return f"reply_to_{str(uuid.uuid4()).replace('-','_')}"
def control_with_reply(self, command, timeout=5):
def control_with_reply(self, command, timeout=5, extra_data=None):
logger.warning('checking {} {} for {}'.format(self.service, command, self.queuename))
reply_queue = Control.generate_reply_queue_name()
self.result = None
with pg_bus_conn() as conn:
with pg_bus_conn(new_connection=True) as conn:
conn.listen(reply_queue)
conn.notify(self.queuename, json.dumps({'control': command, 'reply_to': reply_queue}))
send_data = {'control': command, 'reply_to': reply_queue}
if extra_data:
send_data.update(extra_data)
conn.notify(self.queuename, json.dumps(send_data))
for reply in conn.events(select_timeout=timeout, yield_timeouts=True):
if reply is None:

View File

@@ -16,13 +16,14 @@ from queue import Full as QueueFull, Empty as QueueEmpty
from django.conf import settings
from django.db import connection as django_connection, connections
from django.core.cache import cache as django_cache
from django.utils.timezone import now as tz_now
from django_guid import set_guid
from jinja2 import Template
import psutil
from awx.main.models import UnifiedJob
from awx.main.dispatch import reaper
from awx.main.utils.common import convert_mem_str_to_bytes, get_mem_effective_capacity
from awx.main.utils.common import convert_mem_str_to_bytes, get_mem_effective_capacity, log_excess_runtime
if 'run_callback_receiver' in sys.argv:
logger = logging.getLogger('awx.main.commands.run_callback_receiver')
@@ -328,12 +329,16 @@ class AutoscalePool(WorkerPool):
# Get same number as max forks based on memory, this function takes memory as bytes
self.max_workers = get_mem_effective_capacity(total_memory_gb * 2**30)
# add magic prime number of extra workers to ensure
# we have a few extra workers to run the heartbeat
self.max_workers += 7
# max workers can't be less than min_workers
self.max_workers = max(self.min_workers, self.max_workers)
def debug(self, *args, **kwargs):
self.cleanup()
return super(AutoscalePool, self).debug(*args, **kwargs)
# the task manager enforces settings.TASK_MANAGER_TIMEOUT on its own
# but if the task takes longer than the time defined here, we will force it to stop here
self.task_manager_timeout = settings.TASK_MANAGER_TIMEOUT + settings.TASK_MANAGER_TIMEOUT_GRACE_PERIOD
@property
def should_grow(self):
@@ -351,6 +356,7 @@ class AutoscalePool(WorkerPool):
def debug_meta(self):
return 'min={} max={}'.format(self.min_workers, self.max_workers)
@log_excess_runtime(logger)
def cleanup(self):
"""
Perform some internal account and cleanup. This is run on
@@ -359,8 +365,6 @@ class AutoscalePool(WorkerPool):
1. Discover worker processes that exited, and recover messages they
were handling.
2. Clean up unnecessary, idle workers.
3. Check to see if the database says this node is running any tasks
that aren't actually running. If so, reap them.
IMPORTANT: this function is one of the few places in the dispatcher
(aside from setting lookups) where we talk to the database. As such,
@@ -401,13 +405,15 @@ class AutoscalePool(WorkerPool):
# the task manager to never do more work
current_task = w.current_task
if current_task and isinstance(current_task, dict):
if current_task.get('task', '').endswith('tasks.run_task_manager'):
endings = ['tasks.task_manager', 'tasks.dependency_manager', 'tasks.workflow_manager']
current_task_name = current_task.get('task', '')
if any(current_task_name.endswith(e) for e in endings):
if 'started' not in current_task:
w.managed_tasks[current_task['uuid']]['started'] = time.time()
age = time.time() - current_task['started']
w.managed_tasks[current_task['uuid']]['age'] = age
if age > (60 * 5):
logger.error(f'run_task_manager has held the advisory lock for >5m, sending SIGTERM to {w.pid}') # noqa
if age > self.task_manager_timeout:
logger.error(f'{current_task_name} has held the advisory lock for {age}, sending SIGTERM to {w.pid}')
os.kill(w.pid, signal.SIGTERM)
for m in orphaned:
@@ -417,13 +423,17 @@ class AutoscalePool(WorkerPool):
idx = random.choice(range(len(self.workers)))
self.write(idx, m)
# if the database says a job is running on this node, but it's *not*,
# then reap it
running_uuids = []
for worker in self.workers:
worker.calculate_managed_tasks()
running_uuids.extend(list(worker.managed_tasks.keys()))
reaper.reap(excluded_uuids=running_uuids)
def add_bind_kwargs(self, body):
bind_kwargs = body.pop('bind_kwargs', [])
body.setdefault('kwargs', {})
if 'dispatch_time' in bind_kwargs:
body['kwargs']['dispatch_time'] = tz_now().isoformat()
if 'worker_tasks' in bind_kwargs:
worker_tasks = {}
for worker in self.workers:
worker.calculate_managed_tasks()
worker_tasks[worker.pid] = list(worker.managed_tasks.keys())
body['kwargs']['worker_tasks'] = worker_tasks
def up(self):
if self.full:
@@ -438,6 +448,8 @@ class AutoscalePool(WorkerPool):
if 'guid' in body:
set_guid(body['guid'])
try:
if isinstance(body, dict) and body.get('bind_kwargs'):
self.add_bind_kwargs(body)
# when the cluster heartbeat occurs, clean up internally
if isinstance(body, dict) and 'cluster_node_heartbeat' in body['task']:
self.cleanup()
@@ -452,6 +464,10 @@ class AutoscalePool(WorkerPool):
w.put(body)
break
else:
task_name = 'unknown'
if isinstance(body, dict):
task_name = body.get('task')
logger.warn(f'Workers maxed, queuing {task_name}, load: {sum(len(w.managed_tasks) for w in self.workers)} / {len(self.workers)}')
return super(AutoscalePool, self).write(preferred_queue, body)
except Exception:
for conn in connections.all():

View File

@@ -2,6 +2,7 @@ import inspect
import logging
import sys
import json
import time
from uuid import uuid4
from django.conf import settings
@@ -49,13 +50,21 @@ class task:
@task(queue='tower_broadcast')
def announce():
print("Run this everywhere!")
# The special parameter bind_kwargs tells the main dispatcher process to add certain kwargs
@task(bind_kwargs=['dispatch_time'])
def print_time(dispatch_time=None):
print(f"Time I was dispatched: {dispatch_time}")
"""
def __init__(self, queue=None):
def __init__(self, queue=None, bind_kwargs=None):
self.queue = queue
self.bind_kwargs = bind_kwargs
def __call__(self, fn=None):
queue = self.queue
bind_kwargs = self.bind_kwargs
class PublisherMixin(object):
@@ -75,10 +84,12 @@ class task:
msg = f'{cls.name}: Queue value required and may not be None'
logger.error(msg)
raise ValueError(msg)
obj = {'uuid': task_id, 'args': args, 'kwargs': kwargs, 'task': cls.name}
obj = {'uuid': task_id, 'args': args, 'kwargs': kwargs, 'task': cls.name, 'time_pub': time.time()}
guid = get_guid()
if guid:
obj['guid'] = guid
if bind_kwargs:
obj['bind_kwargs'] = bind_kwargs
obj.update(**kw)
if callable(queue):
queue = queue()

View File

@@ -2,6 +2,7 @@ from datetime import timedelta
import logging
from django.db.models import Q
from django.conf import settings
from django.utils.timezone import now as tz_now
from django.contrib.contenttypes.models import ContentType
@@ -10,28 +11,76 @@ from awx.main.models import Instance, UnifiedJob, WorkflowJob
logger = logging.getLogger('awx.main.dispatch')
def reap_job(j, status):
if UnifiedJob.objects.get(id=j.id).status not in ('running', 'waiting'):
def startup_reaping():
"""
If this particular instance is starting, then we know that any running jobs are invalid
so we will reap those jobs as a special action here
"""
try:
me = Instance.objects.me()
except RuntimeError as e:
logger.warning(f'Local instance is not registered, not running startup reaper: {e}')
return
jobs = UnifiedJob.objects.filter(status='running', controller_node=me.hostname)
job_ids = []
for j in jobs:
job_ids.append(j.id)
reap_job(
j,
'failed',
job_explanation='Task was marked as running at system start up. The system must have not shut down properly, so it has been marked as failed.',
)
if job_ids:
logger.error(f'Unified jobs {job_ids} were reaped on dispatch startup')
def reap_job(j, status, job_explanation=None):
j.refresh_from_db(fields=['status', 'job_explanation'])
status_before = j.status
if status_before not in ('running', 'waiting'):
# just in case, don't reap jobs that aren't running
return
j.status = status
j.start_args = '' # blank field to remove encrypted passwords
j.job_explanation += ' '.join(
(
'Task was marked as running but was not present in',
'the job queue, so it has been marked as failed.',
)
)
if j.job_explanation:
j.job_explanation += ' ' # Separate messages for readability
if job_explanation is None:
j.job_explanation += 'Task was marked as running but was not present in the job queue, so it has been marked as failed.'
else:
j.job_explanation += job_explanation
j.save(update_fields=['status', 'start_args', 'job_explanation'])
if hasattr(j, 'send_notification_templates'):
j.send_notification_templates('failed')
j.websocket_emit_status(status)
logger.error('{} is no longer running; reaping'.format(j.log_format))
logger.error(f'{j.log_format} is no longer {status_before}; reaping')
def reap(instance=None, status='failed', excluded_uuids=[]):
def reap_waiting(instance=None, status='failed', job_explanation=None, grace_period=None, excluded_uuids=None, ref_time=None):
"""
Reap all jobs in waiting|running for this instance.
Reap all jobs in waiting for this instance.
"""
if grace_period is None:
grace_period = settings.JOB_WAITING_GRACE_PERIOD + settings.TASK_MANAGER_TIMEOUT
me = instance
if me is None:
try:
me = Instance.objects.me()
except RuntimeError as e:
logger.warning(f'Local instance is not registered, not running reaper: {e}')
return
if ref_time is None:
ref_time = tz_now()
jobs = UnifiedJob.objects.filter(status='waiting', modified__lte=ref_time - timedelta(seconds=grace_period), controller_node=me.hostname)
if excluded_uuids:
jobs = jobs.exclude(celery_task_id__in=excluded_uuids)
for j in jobs:
reap_job(j, status, job_explanation=job_explanation)
def reap(instance=None, status='failed', job_explanation=None, excluded_uuids=None):
"""
Reap all jobs in running for this instance.
"""
me = instance
if me is None:
@@ -40,12 +89,11 @@ def reap(instance=None, status='failed', excluded_uuids=[]):
except RuntimeError as e:
logger.warning(f'Local instance is not registered, not running reaper: {e}')
return
now = tz_now()
workflow_ctype_id = ContentType.objects.get_for_model(WorkflowJob).id
jobs = UnifiedJob.objects.filter(
(Q(status='running') | Q(status='waiting', modified__lte=now - timedelta(seconds=60)))
& (Q(execution_node=me.hostname) | Q(controller_node=me.hostname))
& ~Q(polymorphic_ctype_id=workflow_ctype_id)
).exclude(celery_task_id__in=excluded_uuids)
Q(status='running') & (Q(execution_node=me.hostname) | Q(controller_node=me.hostname)) & ~Q(polymorphic_ctype_id=workflow_ctype_id)
)
if excluded_uuids:
jobs = jobs.exclude(celery_task_id__in=excluded_uuids)
for j in jobs:
reap_job(j, status)
reap_job(j, status, job_explanation=job_explanation)

View File

@@ -17,6 +17,7 @@ from django.conf import settings
from awx.main.dispatch.pool import WorkerPool
from awx.main.dispatch import pg_bus_conn
from awx.main.utils.common import log_excess_runtime
if 'run_callback_receiver' in sys.argv:
logger = logging.getLogger('awx.main.commands.run_callback_receiver')
@@ -62,7 +63,7 @@ class AWXConsumerBase(object):
def control(self, body):
logger.warning(f'Received control signal:\n{body}')
control = body.get('control')
if control in ('status', 'running'):
if control in ('status', 'running', 'cancel'):
reply_queue = body['reply_to']
if control == 'status':
msg = '\n'.join([self.listening_on, self.pool.debug()])
@@ -71,6 +72,17 @@ class AWXConsumerBase(object):
for worker in self.pool.workers:
worker.calculate_managed_tasks()
msg.extend(worker.managed_tasks.keys())
elif control == 'cancel':
msg = []
task_ids = set(body['task_ids'])
for worker in self.pool.workers:
task = worker.current_task
if task and task['uuid'] in task_ids:
logger.warn(f'Sending SIGTERM to task id={task["uuid"]}, task={task.get("task")}, args={task.get("args")}')
os.kill(worker.pid, signal.SIGTERM)
msg.append(task['uuid'])
if task_ids and not msg:
logger.info(f'Could not locate running tasks to cancel with ids={task_ids}')
with pg_bus_conn() as conn:
conn.notify(reply_queue, json.dumps(msg))
@@ -81,6 +93,9 @@ class AWXConsumerBase(object):
logger.error('unrecognized control message: {}'.format(control))
def process_task(self, body):
if isinstance(body, dict):
body['time_ack'] = time.time()
if 'control' in body:
try:
return self.control(body)
@@ -101,6 +116,7 @@ class AWXConsumerBase(object):
self.total_messages += 1
self.record_statistics()
@log_excess_runtime(logger)
def record_statistics(self):
if time.time() - self.last_stats > 1: # buffer stat recording to once per second
try:
@@ -149,7 +165,7 @@ class AWXConsumerPG(AWXConsumerBase):
while True:
try:
with pg_bus_conn() as conn:
with pg_bus_conn(new_connection=True) as conn:
for queue in self.queues:
conn.listen(queue)
if init is False:
@@ -169,8 +185,9 @@ class AWXConsumerPG(AWXConsumerBase):
logger.exception(f"Error consuming new events from postgres, will retry for {self.pg_max_wait} s")
self.pg_down_time = time.time()
self.pg_is_down = True
if time.time() - self.pg_down_time > self.pg_max_wait:
logger.warning(f"Postgres event consumer has not recovered in {self.pg_max_wait} s, exiting")
current_downtime = time.time() - self.pg_down_time
if current_downtime > self.pg_max_wait:
logger.exception(f"Postgres event consumer has not recovered in {current_downtime} s, exiting")
raise
# Wait for a second before next attempt, but still listen for any shutdown signals
for i in range(10):
@@ -179,6 +196,10 @@ class AWXConsumerPG(AWXConsumerBase):
time.sleep(0.1)
for conn in db.connections.all():
conn.close_if_unusable_or_obsolete()
except Exception:
# Log unanticipated exception in addition to writing to stderr to get timestamps and other metadata
logger.exception('Encountered unhandled error in dispatcher main loop')
raise
class BaseWorker(object):

View File

@@ -4,10 +4,12 @@ import os
import signal
import time
import traceback
import datetime
from django.conf import settings
from django.utils.functional import cached_property
from django.utils.timezone import now as tz_now
from django.db import DatabaseError, OperationalError, connection as django_connection
from django.db import DatabaseError, OperationalError, transaction, connection as django_connection
from django.db.utils import InterfaceError, InternalError
from django_guid import set_guid
@@ -16,8 +18,8 @@ import psutil
import redis
from awx.main.consumers import emit_channel_notification
from awx.main.models import JobEvent, AdHocCommandEvent, ProjectUpdateEvent, InventoryUpdateEvent, SystemJobEvent, UnifiedJob, Job
from awx.main.tasks.system import handle_success_and_failure_notifications
from awx.main.models import JobEvent, AdHocCommandEvent, ProjectUpdateEvent, InventoryUpdateEvent, SystemJobEvent, UnifiedJob
from awx.main.constants import ACTIVE_STATES
from awx.main.models.events import emit_event_detail
from awx.main.utils.profiling import AWXProfiler
import awx.main.analytics.subsystem_metrics as s_metrics
@@ -26,6 +28,32 @@ from .base import BaseWorker
logger = logging.getLogger('awx.main.commands.run_callback_receiver')
def job_stats_wrapup(job_identifier, event=None):
"""Fill in the unified job host_status_counts, fire off notifications if needed"""
try:
# empty dict (versus default of None) can still indicate that events have been processed
# for job types like system jobs, and jobs with no hosts matched
host_status_counts = {}
if event:
host_status_counts = event.get_host_status_counts()
# Update host_status_counts while holding the row lock
with transaction.atomic():
uj = UnifiedJob.objects.select_for_update().get(pk=job_identifier)
uj.host_status_counts = host_status_counts
uj.save(update_fields=['host_status_counts'])
uj.log_lifecycle("stats_wrapup_finished")
# If the status was a finished state before this update was made, send notifications
# If not, we will send notifications when the status changes
if uj.status not in ACTIVE_STATES:
uj.send_notification_templates('succeeded' if uj.status == 'successful' else 'failed')
except Exception:
logger.exception('Worker failed to save stats or emit notifications: Job {}'.format(job_identifier))
class CallbackBrokerWorker(BaseWorker):
"""
A worker implementation that deserializes callback event data and persists
@@ -44,7 +72,6 @@ class CallbackBrokerWorker(BaseWorker):
def __init__(self):
self.buff = {}
self.pid = os.getpid()
self.redis = redis.Redis.from_url(settings.BROKER_URL)
self.subsystem_metrics = s_metrics.Metrics(auto_pipe_execute=False)
self.queue_pop = 0
@@ -53,6 +80,11 @@ class CallbackBrokerWorker(BaseWorker):
for key in self.redis.keys('awx_callback_receiver_statistics_*'):
self.redis.delete(key)
@cached_property
def pid(self):
"""This needs to be obtained after forking, or else it will give the parent process"""
return os.getpid()
def read(self, queue):
try:
res = self.redis.blpop(self.queue_name, timeout=1)
@@ -120,32 +152,49 @@ class CallbackBrokerWorker(BaseWorker):
metrics_singular_events_saved = 0
metrics_events_batch_save_errors = 0
metrics_events_broadcast = 0
metrics_events_missing_created = 0
metrics_total_job_event_processing_seconds = datetime.timedelta(seconds=0)
for cls, events in self.buff.items():
logger.debug(f'{cls.__name__}.objects.bulk_create({len(events)})')
for e in events:
e.modified = now # this can be set before created because now is set above on line 149
if not e.created:
e.created = now
e.modified = now
metrics_events_missing_created += 1
else: # only calculate the seconds if the created time already has been set
metrics_total_job_event_processing_seconds += e.modified - e.created
metrics_duration_to_save = time.perf_counter()
try:
cls.objects.bulk_create(events)
metrics_bulk_events_saved += len(events)
except Exception:
except Exception as exc:
logger.warning(f'Error in events bulk_create, will try indiviually up to 5 errors, error {str(exc)}')
# if an exception occurs, we should re-attempt to save the
# events one-by-one, because something in the list is
# broken/stale
consecutive_errors = 0
events_saved = 0
metrics_events_batch_save_errors += 1
for e in events:
try:
e.save()
metrics_singular_events_saved += 1
except Exception:
logger.exception('Database Error Saving Job Event')
events_saved += 1
consecutive_errors = 0
except Exception as exc_indv:
consecutive_errors += 1
logger.info(f'Database Error Saving individual Job Event, error {str(exc_indv)}')
if consecutive_errors >= 5:
raise
metrics_singular_events_saved += events_saved
if events_saved == 0:
raise
metrics_duration_to_save = time.perf_counter() - metrics_duration_to_save
for e in events:
if not getattr(e, '_skip_websocket_message', False):
metrics_events_broadcast += 1
emit_event_detail(e)
if getattr(e, '_notification_trigger_event', False):
job_stats_wrapup(getattr(e, e.JOB_REFERENCE), event=e)
self.buff = {}
self.last_flush = time.time()
# only update metrics if we saved events
@@ -156,6 +205,11 @@ class CallbackBrokerWorker(BaseWorker):
self.subsystem_metrics.observe('callback_receiver_batch_events_insert_db', metrics_bulk_events_saved)
self.subsystem_metrics.inc('callback_receiver_events_in_memory', -(metrics_bulk_events_saved + metrics_singular_events_saved))
self.subsystem_metrics.inc('callback_receiver_events_broadcast', metrics_events_broadcast)
self.subsystem_metrics.set(
'callback_receiver_event_processing_avg_seconds',
metrics_total_job_event_processing_seconds.total_seconds()
/ (metrics_bulk_events_saved + metrics_singular_events_saved - metrics_events_missing_created),
)
if self.subsystem_metrics.should_pipe_execute() is True:
self.subsystem_metrics.pipe_execute()
@@ -165,47 +219,32 @@ class CallbackBrokerWorker(BaseWorker):
if flush:
self.last_event = ''
if not flush:
event_map = {
'job_id': JobEvent,
'ad_hoc_command_id': AdHocCommandEvent,
'project_update_id': ProjectUpdateEvent,
'inventory_update_id': InventoryUpdateEvent,
'system_job_id': SystemJobEvent,
}
job_identifier = 'unknown job'
for key, cls in event_map.items():
if key in body:
job_identifier = body[key]
for cls in (JobEvent, AdHocCommandEvent, ProjectUpdateEvent, InventoryUpdateEvent, SystemJobEvent):
if cls.JOB_REFERENCE in body:
job_identifier = body[cls.JOB_REFERENCE]
break
self.last_event = f'\n\t- {cls.__name__} for #{job_identifier} ({body.get("event", "")} {body.get("uuid", "")})' # noqa
notification_trigger_event = bool(body.get('event') == cls.WRAPUP_EVENT)
if body.get('event') == 'EOF':
try:
if 'guid' in body:
set_guid(body['guid'])
final_counter = body.get('final_counter', 0)
logger.info('Event processing is finished for Job {}, sending notifications'.format(job_identifier))
logger.info('Starting EOF event processing for Job {}'.format(job_identifier))
# EOF events are sent when stdout for the running task is
# closed. don't actually persist them to the database; we
# just use them to report `summary` websocket events as an
# approximation for when a job is "done"
emit_channel_notification('jobs-summary', dict(group_name='jobs', unified_job_id=job_identifier, final_counter=final_counter))
# Additionally, when we've processed all events, we should
# have all the data we need to send out success/failure
# notification templates
uj = UnifiedJob.objects.get(pk=job_identifier)
if isinstance(uj, Job):
# *actual playbooks* send their success/failure
# notifications in response to the playbook_on_stats
# event handling code in main.models.events
pass
elif hasattr(uj, 'send_notification_templates'):
handle_success_and_failure_notifications.apply_async([uj.id])
if notification_trigger_event:
job_stats_wrapup(job_identifier)
except Exception:
logger.exception('Worker failed to emit notifications: Job {}'.format(job_identifier))
logger.exception('Worker failed to perform EOF tasks: Job {}'.format(job_identifier))
finally:
self.subsystem_metrics.inc('callback_receiver_events_in_memory', -1)
set_guid('')
@@ -215,9 +254,12 @@ class CallbackBrokerWorker(BaseWorker):
event = cls.create_from_data(**body)
if skip_websocket_message:
if skip_websocket_message: # if this event sends websocket messages, fire them off on flush
event._skip_websocket_message = True
if notification_trigger_event: # if this is an Ansible stats event, ensure notifications on flush
event._notification_trigger_event = True
self.buff.setdefault(cls, []).append(event)
retries = 0
@@ -225,17 +267,18 @@ class CallbackBrokerWorker(BaseWorker):
try:
self.flush(force=flush)
break
except (OperationalError, InterfaceError, InternalError):
except (OperationalError, InterfaceError, InternalError) as exc:
if retries >= self.MAX_RETRIES:
logger.exception('Worker could not re-establish database connectivity, giving up on one or more events.')
return
delay = 60 * retries
logger.exception('Database Error Saving Job Event, retry #{i} in {delay} seconds:'.format(i=retries + 1, delay=delay))
logger.warning(f'Database Error Flushing Job Events, retry #{retries + 1} in {delay} seconds: {str(exc)}')
django_connection.close()
time.sleep(delay)
retries += 1
except DatabaseError:
logger.exception('Database Error Saving Job Event')
logger.exception('Database Error Flushing Job Events')
django_connection.close()
break
except Exception as exc:
tb = traceback.format_exc()

View File

@@ -3,6 +3,7 @@ import logging
import importlib
import sys
import traceback
import time
from kubernetes.config import kube_config
@@ -60,8 +61,19 @@ class TaskWorker(BaseWorker):
# the callable is a class, e.g., RunJob; instantiate and
# return its `run()` method
_call = _call().run
log_extra = ''
logger_method = logger.debug
if ('time_ack' in body) and ('time_pub' in body):
time_publish = body['time_ack'] - body['time_pub']
time_waiting = time.time() - body['time_ack']
if time_waiting > 5.0 or time_publish > 5.0:
# If task too a very long time to process, add this information to the log
log_extra = f' took {time_publish:.4f} to ack, {time_waiting:.4f} in local dispatcher'
logger_method = logger.info
# don't print kwargs, they often contain launch-time secrets
logger.debug('task {} starting {}(*{})'.format(uuid, task, args))
logger_method(f'task {uuid} starting {task}(*{args}){log_extra}')
return _call(*args, **kwargs)
def perform_work(self, body):

View File

@@ -103,7 +103,7 @@ class DeleteMeta:
with connection.cursor() as cursor:
query = "SELECT inhrelid::regclass::text AS child FROM pg_catalog.pg_inherits"
query += f" WHERE inhparent = 'public.{tbl_name}'::regclass"
query += f" WHERE inhparent = '{tbl_name}'::regclass"
query += f" AND TO_TIMESTAMP(LTRIM(inhrelid::regclass::text, '{tbl_name}_'), 'YYYYMMDD_HH24') < '{self.cutoff}'"
query += " ORDER BY inhrelid::regclass::text"

View File

@@ -32,8 +32,10 @@ class Command(BaseCommand):
name='Demo Project',
scm_type='git',
scm_url='https://github.com/ansible/ansible-tower-samples',
scm_update_on_launch=True,
scm_update_cache_timeout=0,
status='successful',
scm_revision='347e44fea036c94d5f60e544de006453ee5c71ad',
playbook_files=['hello_world.yml'],
)
p.organization = o

View File

@@ -862,7 +862,7 @@ class Command(BaseCommand):
overwrite_vars=bool(options.get('overwrite_vars', False)),
)
inventory_update = inventory_source.create_inventory_update(
_eager_fields=dict(job_args=json.dumps(sys.argv), job_env=dict(os.environ.items()), job_cwd=os.getcwd())
_eager_fields=dict(status='running', job_args=json.dumps(sys.argv), job_env=dict(os.environ.items()), job_cwd=os.getcwd())
)
data = AnsibleInventoryLoader(source=source, verbosity=verbosity).load()

View File

@@ -54,7 +54,7 @@ class Command(BaseCommand):
capacity = f' capacity={x.capacity}' if x.node_type != 'hop' else ''
version = f" version={x.version or '?'}" if x.node_type != 'hop' else ''
heartbeat = f' heartbeat="{x.modified:%Y-%m-%d %H:%M:%S}"' if x.capacity or x.node_type == 'hop' else ''
heartbeat = f' heartbeat="{x.last_seen:%Y-%m-%d %H:%M:%S}"' if x.capacity or x.node_type == 'hop' else ''
print(f'\t{color}{x.hostname}{capacity} node_type={x.node_type}{version}{heartbeat}\033[0m')
print()

View File

@@ -1,13 +1,14 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
import logging
import yaml
from django.conf import settings
from django.core.cache import cache as django_cache
from django.core.management.base import BaseCommand
from django.db import connection as django_connection
from awx.main.dispatch import get_local_queuename, reaper
from awx.main.dispatch import get_local_queuename
from awx.main.dispatch.control import Control
from awx.main.dispatch.pool import AutoscalePool
from awx.main.dispatch.worker import AWXConsumerPG, TaskWorker
@@ -30,7 +31,16 @@ class Command(BaseCommand):
'--reload',
dest='reload',
action='store_true',
help=('cause the dispatcher to recycle all of its worker processes;' 'running jobs will run to completion first'),
help=('cause the dispatcher to recycle all of its worker processes; running jobs will run to completion first'),
)
parser.add_argument(
'--cancel',
dest='cancel',
help=(
'Cancel a particular task id. Takes either a single id string, or a JSON list of multiple ids. '
'Can take in output from the --running argument as input to cancel all tasks. '
'Only running tasks can be canceled, queued tasks must be started before they can be canceled.'
),
)
def handle(self, *arg, **options):
@@ -42,6 +52,16 @@ class Command(BaseCommand):
return
if options.get('reload'):
return Control('dispatcher').control({'control': 'reload'})
if options.get('cancel'):
cancel_str = options.get('cancel')
try:
cancel_data = yaml.safe_load(cancel_str)
except Exception:
cancel_data = [cancel_str]
if not isinstance(cancel_data, list):
cancel_data = [cancel_str]
print(Control('dispatcher').cancel(cancel_data))
return
# It's important to close these because we're _about_ to fork, and we
# don't want the forked processes to inherit the open sockets
@@ -53,7 +73,6 @@ class Command(BaseCommand):
# (like the node heartbeat)
periodic.run_continuously()
reaper.reap()
consumer = None
try:

View File

@@ -95,8 +95,13 @@ class Command(BaseCommand):
# database migrations are still running
from awx.main.models.ha import Instance
executor = MigrationExecutor(connection)
migrating = bool(executor.migration_plan(executor.loader.graph.leaf_nodes()))
try:
executor = MigrationExecutor(connection)
migrating = bool(executor.migration_plan(executor.loader.graph.leaf_nodes()))
except Exception as exc:
logger.info(f'Error on startup of run_wsbroadcast (error: {exc}), retry in 10s...')
time.sleep(10)
return
# In containerized deployments, migrations happen in the task container,
# and the services running there don't start until migrations are

View File

@@ -26,6 +26,17 @@ logger = logging.getLogger('awx.main.middleware')
perf_logger = logging.getLogger('awx.analytics.performance')
class SettingsCacheMiddleware(MiddlewareMixin):
"""
Clears the in-memory settings cache at the beginning of a request.
We do this so that a script can POST to /api/v2/settings/all/ and then
right away GET /api/v2/settings/all/ and see the updated value.
"""
def process_request(self, request):
settings._awx_conf_memoizedcache.clear()
class TimingMiddleware(threading.local, MiddlewareMixin):
dest = '/var/log/tower/profile'

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-04-18 21:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0159_deprecate_inventory_source_UoPU_field'),
]
operations = [
migrations.AlterField(
model_name='schedule',
name='rrule',
field=models.TextField(help_text='A value representing the schedules iCal recurrence rule.'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-04-27 02:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0160_alter_schedule_rrule'),
]
operations = [
migrations.AddField(
model_name='unifiedjob',
name='host_status_counts',
field=models.JSONField(blank=True, default=None, editable=False, help_text='Playbook stats from the Ansible playbook_on_stats event.', null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.13 on 2022-05-02 21:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0161_unifiedjob_host_status_counts'),
]
operations = [
migrations.AlterField(
model_name='unifiedjob',
name='dependent_jobs',
field=models.ManyToManyField(editable=False, related_name='unifiedjob_blocked_jobs', to='main.UnifiedJob'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.13 on 2022-06-02 18:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0162_alter_unifiedjob_dependent_jobs'),
]
operations = [
migrations.AlterField(
model_name='job',
name='job_tags',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='jobtemplate',
name='job_tags',
field=models.TextField(blank=True, default=''),
),
]

View File

@@ -0,0 +1,40 @@
# Generated by Django 3.2.13 on 2022-06-21 21:29
from django.db import migrations
import logging
logger = logging.getLogger("awx")
def forwards(apps, schema_editor):
InventorySource = apps.get_model('main', 'InventorySource')
sources = InventorySource.objects.filter(update_on_project_update=True)
for src in sources:
if src.update_on_launch == False:
src.update_on_launch = True
src.save(update_fields=['update_on_launch'])
logger.info(f"Setting update_on_launch to True for {src}")
proj = src.source_project
if proj and proj.scm_update_on_launch is False:
proj.scm_update_on_launch = True
proj.save(update_fields=['scm_update_on_launch'])
logger.warning(f"Setting scm_update_on_launch to True for {proj}")
class Migration(migrations.Migration):
dependencies = [
('main', '0163_convert_job_tags_to_textfield'),
]
operations = [
migrations.RunPython(forwards, migrations.RunPython.noop),
migrations.RemoveField(
model_name='inventorysource',
name='scm_last_revision',
),
migrations.RemoveField(
model_name='inventorysource',
name='update_on_project_update',
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 3.2.13 on 2022-08-10 14:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0164_remove_inventorysource_update_on_project_update'),
]
operations = [
migrations.AddField(
model_name='unifiedjob',
name='preferred_instance_groups_cache',
field=models.JSONField(
blank=True, default=None, editable=False, help_text='A cached list with pk values from preferred instance groups.', null=True
),
),
migrations.AddField(
model_name='unifiedjob',
name='task_impact',
field=models.PositiveIntegerField(default=0, editable=False, help_text='Number of forks an instance consumes when running this job.'),
),
migrations.AddField(
model_name='workflowapproval',
name='expires',
field=models.DateTimeField(
default=None,
editable=False,
help_text='The time this approval will expire. This is the created time plus timeout, used for filtering.',
null=True,
),
),
]

View File

@@ -0,0 +1,40 @@
# Generated by Django 3.2.13 on 2022-07-06 13:19
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0165_task_manager_refactor'),
]
operations = [
migrations.AlterField(
model_name='adhoccommandevent',
name='host',
field=models.ForeignKey(
db_constraint=False,
default=None,
editable=False,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='ad_hoc_command_events',
to='main.host',
),
),
migrations.AlterField(
model_name='jobevent',
name='host',
field=models.ForeignKey(
db_constraint=False,
default=None,
editable=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name='job_events_as_primary_host',
to='main.host',
),
),
]

View File

@@ -0,0 +1,57 @@
# Generated by Django 3.2.13 on 2022-08-24 14:02
from django.db import migrations, models
import django.db.models.deletion
from awx.main.models import CredentialType
from awx.main.utils.common import set_current_apps
def setup_tower_managed_defaults(apps, schema_editor):
set_current_apps(apps)
CredentialType.setup_tower_managed_defaults(apps)
class Migration(migrations.Migration):
dependencies = [
('main', '0166_alter_jobevent_host'),
]
operations = [
migrations.AddField(
model_name='project',
name='signature_validation_credential',
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='projects_signature_validation',
to='main.credential',
help_text='An optional credential used for validating files in the project against unexpected changes.',
),
),
migrations.AlterField(
model_name='credentialtype',
name='kind',
field=models.CharField(
choices=[
('ssh', 'Machine'),
('vault', 'Vault'),
('net', 'Network'),
('scm', 'Source Control'),
('cloud', 'Cloud'),
('registry', 'Container Registry'),
('token', 'Personal Access Token'),
('insights', 'Insights'),
('external', 'External'),
('kubernetes', 'Kubernetes'),
('galaxy', 'Galaxy/Automation Hub'),
('cryptography', 'Cryptography'),
],
max_length=32,
),
),
migrations.RunPython(setup_tower_managed_defaults),
]

View File

@@ -36,7 +36,7 @@ def create_clearsessions_jt(apps, schema_editor):
if created:
sched = Schedule(
name='Cleanup Expired Sessions',
rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1' % schedule_time,
rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1' % schedule_time,
description='Cleans out expired browser sessions',
enabled=True,
created=now_dt,
@@ -69,7 +69,7 @@ def create_cleartokens_jt(apps, schema_editor):
if created:
sched = Schedule(
name='Cleanup Expired OAuth 2 Tokens',
rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1' % schedule_time,
rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1' % schedule_time,
description='Removes expired OAuth 2 access and refresh tokens',
enabled=True,
created=now_dt,

View File

@@ -90,6 +90,9 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
extra_vars_dict = VarsDictProperty('extra_vars', True)
def _set_default_dependencies_processed(self):
self.dependencies_processed = True
def clean_inventory(self):
inv = self.inventory
if not inv:
@@ -178,12 +181,12 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
def get_passwords_needed_to_start(self):
return self.passwords_needed_to_start
@property
def task_impact(self):
def _get_task_impact(self):
# NOTE: We sorta have to assume the host count matches and that forks default to 5
from awx.main.models.inventory import Host
count_hosts = Host.objects.filter(enabled=True, inventory__ad_hoc_commands__pk=self.pk).count()
if self.inventory:
count_hosts = self.inventory.total_hosts
else:
count_hosts = 5
return min(count_hosts, 5 if self.forks == 0 else self.forks) + 1
def copy(self):
@@ -207,10 +210,20 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', [])
def add_to_update_fields(name):
if name not in update_fields:
update_fields.append(name)
if not self.preferred_instance_groups_cache:
self.preferred_instance_groups_cache = self._get_preferred_instance_group_cache()
add_to_update_fields("preferred_instance_groups_cache")
if not self.name:
self.name = Truncator(u': '.join(filter(None, (self.module_name, self.module_args)))).chars(512)
if 'name' not in update_fields:
update_fields.append('name')
add_to_update_fields("name")
if self.task_impact == 0:
self.task_impact = self._get_task_impact()
add_to_update_fields("task_impact")
super(AdHocCommand, self).save(*args, **kwargs)
@property

View File

@@ -316,16 +316,17 @@ class PrimordialModel(HasEditsMixin, CreatedModifiedModel):
user = get_current_user()
if user and not user.id:
user = None
if not self.pk and not self.created_by:
if (not self.pk) and (user is not None) and (not self.created_by):
self.created_by = user
if 'created_by' not in update_fields:
update_fields.append('created_by')
# Update modified_by if any editable fields have changed
new_values = self._get_fields_snapshot()
if (not self.pk and not self.modified_by) or self._values_have_edits(new_values):
self.modified_by = user
if 'modified_by' not in update_fields:
update_fields.append('modified_by')
if self.modified_by != user:
self.modified_by = user
if 'modified_by' not in update_fields:
update_fields.append('modified_by')
super(PrimordialModel, self).save(*args, **kwargs)
self._prior_values_store = new_values

View File

@@ -336,6 +336,7 @@ class CredentialType(CommonModelNameNotUnique):
('external', _('External')),
('kubernetes', _('Kubernetes')),
('galaxy', _('Galaxy/Automation Hub')),
('cryptography', _('Cryptography')),
)
kind = models.CharField(max_length=32, choices=KIND_CHOICES)
@@ -1171,6 +1172,25 @@ ManagedCredentialType(
},
)
ManagedCredentialType(
namespace='gpg_public_key',
kind='cryptography',
name=gettext_noop('GPG Public Key'),
inputs={
'fields': [
{
'id': 'gpg_public_key',
'label': gettext_noop('GPG Public Key'),
'type': 'string',
'secret': True,
'multiline': True,
'help_text': gettext_noop('GPG Public Key used to validate content signatures.'),
},
],
'required': ['gpg_public_key'],
},
)
class CredentialInputSource(PrimordialModel):
class Meta:

View File

@@ -35,6 +35,7 @@ def gce(cred, env, private_data_dir):
container_path = to_container_path(path, private_data_dir)
env['GCE_CREDENTIALS_FILE_PATH'] = container_path
env['GCP_SERVICE_ACCOUNT_FILE'] = container_path
env['GOOGLE_APPLICATION_CREDENTIALS'] = container_path
# Handle env variables for new module types.
# This includes gcp_compute inventory plugin and

View File

@@ -6,7 +6,7 @@ from collections import defaultdict
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db import models, DatabaseError, connection
from django.db import models, DatabaseError
from django.utils.dateparse import parse_datetime
from django.utils.text import Truncator
from django.utils.timezone import utc, now
@@ -25,7 +25,6 @@ analytics_logger = logging.getLogger('awx.analytics.job_events')
logger = logging.getLogger('awx.main.models.events')
__all__ = ['JobEvent', 'ProjectUpdateEvent', 'AdHocCommandEvent', 'InventoryUpdateEvent', 'SystemJobEvent']
@@ -126,6 +125,7 @@ class BasePlaybookEvent(CreatedModifiedModel):
'host_name',
'verbosity',
]
WRAPUP_EVENT = 'playbook_on_stats'
class Meta:
abstract = True
@@ -384,14 +384,6 @@ class BasePlaybookEvent(CreatedModifiedModel):
job.get_event_queryset().filter(uuid__in=changed).update(changed=True)
job.get_event_queryset().filter(uuid__in=failed).update(failed=True)
# send success/failure notifications when we've finished handling the playbook_on_stats event
from awx.main.tasks.system import handle_success_and_failure_notifications # circular import
def _send_notifications():
handle_success_and_failure_notifications.apply_async([job.id])
connection.on_commit(_send_notifications)
for field in ('playbook', 'play', 'task', 'role'):
value = force_str(event_data.get(field, '')).strip()
if value != getattr(self, field):
@@ -470,6 +462,7 @@ class JobEvent(BasePlaybookEvent):
"""
VALID_KEYS = BasePlaybookEvent.VALID_KEYS + ['job_id', 'workflow_job_id', 'job_created']
JOB_REFERENCE = 'job_id'
objects = DeferJobCreatedManager()
@@ -492,13 +485,18 @@ class JobEvent(BasePlaybookEvent):
editable=False,
db_index=False,
)
# When we partitioned the table we accidentally "lost" the foreign key constraint.
# However this is good because the cascade on delete at the django layer was causing DB issues
# We are going to leave this as a foreign key but mark it as not having a DB relation and
# prevent cascading on delete.
host = models.ForeignKey(
'Host',
related_name='job_events_as_primary_host',
null=True,
default=None,
on_delete=models.SET_NULL,
on_delete=models.DO_NOTHING,
editable=False,
db_constraint=False,
)
host_name = models.CharField(
max_length=1024,
@@ -600,6 +598,7 @@ UnpartitionedJobEvent._meta.db_table = '_unpartitioned_' + JobEvent._meta.db_tab
class ProjectUpdateEvent(BasePlaybookEvent):
VALID_KEYS = BasePlaybookEvent.VALID_KEYS + ['project_update_id', 'workflow_job_id', 'job_created']
JOB_REFERENCE = 'project_update_id'
objects = DeferJobCreatedManager()
@@ -641,6 +640,7 @@ class BaseCommandEvent(CreatedModifiedModel):
"""
VALID_KEYS = ['event_data', 'created', 'counter', 'uuid', 'stdout', 'start_line', 'end_line', 'verbosity']
WRAPUP_EVENT = 'EOF'
class Meta:
abstract = True
@@ -736,6 +736,8 @@ class BaseCommandEvent(CreatedModifiedModel):
class AdHocCommandEvent(BaseCommandEvent):
VALID_KEYS = BaseCommandEvent.VALID_KEYS + ['ad_hoc_command_id', 'event', 'host_name', 'host_id', 'workflow_job_id', 'job_created']
WRAPUP_EVENT = 'playbook_on_stats' # exception to BaseCommandEvent
JOB_REFERENCE = 'ad_hoc_command_id'
objects = DeferJobCreatedManager()
@@ -796,6 +798,10 @@ class AdHocCommandEvent(BaseCommandEvent):
editable=False,
db_index=False,
)
# We need to keep this as a FK in the model because AdHocCommand uses a ManyToMany field
# to hosts through adhoc_events. But in https://github.com/ansible/awx/pull/8236/ we
# removed the nulling of the field in case of a host going away before an event is saved
# so this needs to stay SET_NULL on the ORM level
host = models.ForeignKey(
'Host',
related_name='ad_hoc_command_events',
@@ -803,6 +809,7 @@ class AdHocCommandEvent(BaseCommandEvent):
default=None,
on_delete=models.SET_NULL,
editable=False,
db_constraint=False,
)
host_name = models.CharField(
max_length=1024,
@@ -836,6 +843,7 @@ UnpartitionedAdHocCommandEvent._meta.db_table = '_unpartitioned_' + AdHocCommand
class InventoryUpdateEvent(BaseCommandEvent):
VALID_KEYS = BaseCommandEvent.VALID_KEYS + ['inventory_update_id', 'workflow_job_id', 'job_created']
JOB_REFERENCE = 'inventory_update_id'
objects = DeferJobCreatedManager()
@@ -881,6 +889,7 @@ UnpartitionedInventoryUpdateEvent._meta.db_table = '_unpartitioned_' + Inventory
class SystemJobEvent(BaseCommandEvent):
VALID_KEYS = BaseCommandEvent.VALID_KEYS + ['system_job_id', 'job_created']
JOB_REFERENCE = 'system_job_id'
objects = DeferJobCreatedManager()

View File

@@ -12,6 +12,7 @@ from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.utils.timezone import now, timedelta
from django.db.models import Sum
import redis
from solo.models import SingletonModel
@@ -149,10 +150,13 @@ class Instance(HasPolicyEditsMixin, BaseModel):
def consumed_capacity(self):
capacity_consumed = 0
if self.node_type in ('hybrid', 'execution'):
capacity_consumed += sum(x.task_impact for x in UnifiedJob.objects.filter(execution_node=self.hostname, status__in=('running', 'waiting')))
capacity_consumed += (
UnifiedJob.objects.filter(execution_node=self.hostname, status__in=('running', 'waiting')).aggregate(Sum("task_impact"))["task_impact__sum"]
or 0
)
if self.node_type in ('hybrid', 'control'):
capacity_consumed += sum(
settings.AWX_CONTROL_NODE_TASK_IMPACT for x in UnifiedJob.objects.filter(controller_node=self.hostname, status__in=('running', 'waiting'))
capacity_consumed += (
settings.AWX_CONTROL_NODE_TASK_IMPACT * UnifiedJob.objects.filter(controller_node=self.hostname, status__in=('running', 'waiting')).count()
)
return capacity_consumed
@@ -203,7 +207,7 @@ class Instance(HasPolicyEditsMixin, BaseModel):
return True
if ref_time is None:
ref_time = now()
grace_period = settings.CLUSTER_NODE_HEARTBEAT_PERIOD * 2
grace_period = settings.CLUSTER_NODE_HEARTBEAT_PERIOD * settings.CLUSTER_NODE_MISSED_HEARTBEAT_TOLERANCE
if self.node_type in ('execution', 'hop'):
grace_period += settings.RECEPTOR_SERVICE_ADVERTISEMENT_PERIOD
return self.last_seen < ref_time - timedelta(seconds=grace_period)

View File

@@ -236,6 +236,12 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin):
raise ParseError(_('Slice number must be 1 or higher.'))
return (number, step)
def get_sliced_hosts(self, host_queryset, slice_number, slice_count):
if slice_count > 1 and slice_number > 0:
offset = slice_number - 1
host_queryset = host_queryset[offset::slice_count]
return host_queryset
def get_script_data(self, hostvars=False, towervars=False, show_all=False, slice_number=1, slice_count=1):
hosts_kw = dict()
if not show_all:
@@ -243,10 +249,8 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin):
fetch_fields = ['name', 'id', 'variables', 'inventory_id']
if towervars:
fetch_fields.append('enabled')
hosts = self.hosts.filter(**hosts_kw).order_by('name').only(*fetch_fields)
if slice_count > 1 and slice_number > 0:
offset = slice_number - 1
hosts = hosts[offset::slice_count]
host_queryset = self.hosts.filter(**hosts_kw).order_by('name').only(*fetch_fields)
hosts = self.get_sliced_hosts(host_queryset, slice_number, slice_count)
data = dict()
all_group = data.setdefault('all', dict())
@@ -337,9 +341,12 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin):
else:
active_inventory_sources = self.inventory_sources.filter(source__in=CLOUD_INVENTORY_SOURCES)
failed_inventory_sources = active_inventory_sources.filter(last_job_failed=True)
total_hosts = active_hosts.count()
# if total_hosts has changed, set update_task_impact to True
update_task_impact = total_hosts != self.total_hosts
computed_fields = {
'has_active_failures': bool(failed_hosts.count()),
'total_hosts': active_hosts.count(),
'total_hosts': total_hosts,
'hosts_with_active_failures': failed_hosts.count(),
'total_groups': active_groups.count(),
'has_inventory_sources': bool(active_inventory_sources.count()),
@@ -357,6 +364,14 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin):
computed_fields.pop(field)
if computed_fields:
iobj.save(update_fields=computed_fields.keys())
if update_task_impact:
# if total hosts count has changed, re-calculate task_impact for any
# job that is still in pending for this inventory, since task_impact
# is cached on task creation and used in task management system
tasks = self.jobs.filter(status="pending")
for t in tasks:
t.task_impact = t._get_task_impact()
UnifiedJob.objects.bulk_update(tasks, ['task_impact'])
logger.debug("Finished updating inventory computed fields, pk={0}, in " "{1:.3f} seconds".format(self.pk, time.time() - start_time))
def websocket_emit_status(self, status):
@@ -985,22 +1000,11 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE
default=None,
null=True,
)
scm_last_revision = models.CharField(
max_length=1024,
blank=True,
default='',
editable=False,
)
update_on_project_update = models.BooleanField(
default=False,
help_text=_(
'This field is deprecated and will be removed in a future release. '
'In future release, functionality will be migrated to source project update_on_launch.'
),
)
update_on_launch = models.BooleanField(
default=False,
)
update_cache_timeout = models.PositiveIntegerField(
default=0,
)
@@ -1038,14 +1042,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE
self.name = 'inventory source (%s)' % replace_text
if 'name' not in update_fields:
update_fields.append('name')
# Reset revision if SCM source has changed parameters
if self.source == 'scm' and not is_new_instance:
before_is = self.__class__.objects.get(pk=self.pk)
if before_is.source_path != self.source_path or before_is.source_project_id != self.source_project_id:
# Reset the scm_revision if file changed to force update
self.scm_last_revision = ''
if 'scm_last_revision' not in update_fields:
update_fields.append('scm_last_revision')
# Do the actual save.
super(InventorySource, self).save(*args, **kwargs)
@@ -1054,10 +1050,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE
if replace_text in self.name:
self.name = self.name.replace(replace_text, str(self.pk))
super(InventorySource, self).save(update_fields=['name'])
if self.source == 'scm' and is_new_instance and self.update_on_project_update:
# Schedule a new Project update if one is not already queued
if self.source_project and not self.source_project.project_updates.filter(status__in=['new', 'pending', 'waiting']).exists():
self.update()
if not getattr(_inventory_updates, 'is_updating', False):
if self.inventory is not None:
self.inventory.update_computed_fields()
@@ -1147,25 +1139,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE
)
return dict(error=list(error_notification_templates), started=list(started_notification_templates), success=list(success_notification_templates))
def clean_update_on_project_update(self):
if (
self.update_on_project_update is True
and self.source == 'scm'
and InventorySource.objects.filter(Q(inventory=self.inventory, update_on_project_update=True, source='scm') & ~Q(id=self.id)).exists()
):
raise ValidationError(_("More than one SCM-based inventory source with update on project update per-inventory not allowed."))
return self.update_on_project_update
def clean_update_on_launch(self):
if self.update_on_project_update is True and self.source == 'scm' and self.update_on_launch is True:
raise ValidationError(
_(
"Cannot update SCM-based inventory source on launch if set to update on project update. "
"Instead, configure the corresponding source project to update on launch."
)
)
return self.update_on_launch
def clean_source_path(self):
if self.source != 'scm' and self.source_path:
raise ValidationError(_("Cannot set source_path if not SCM type."))
@@ -1262,8 +1235,7 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin,
return UnpartitionedInventoryUpdateEvent
return InventoryUpdateEvent
@property
def task_impact(self):
def _get_task_impact(self):
return 1
# InventoryUpdate credential required
@@ -1301,13 +1273,6 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin,
return self.global_instance_groups
return selected_groups
def cancel(self, job_explanation=None, is_chain=False):
res = super(InventoryUpdate, self).cancel(job_explanation=job_explanation, is_chain=is_chain)
if res:
if self.launch_type != 'scm' and self.source_project_update:
self.source_project_update.cancel(job_explanation=job_explanation)
return res
class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin):
class Meta:

View File

@@ -130,8 +130,7 @@ class JobOptions(BaseModel):
)
)
)
job_tags = models.CharField(
max_length=1024,
job_tags = models.TextField(
blank=True,
default='',
)
@@ -601,6 +600,19 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
def get_ui_url(self):
return urljoin(settings.TOWER_URL_BASE, "/#/jobs/playbook/{}".format(self.pk))
def _set_default_dependencies_processed(self):
"""
This sets the initial value of dependencies_processed
and here we use this as a shortcut to avoid the DependencyManager for jobs that do not need it
"""
if (not self.project) or self.project.scm_update_on_launch:
self.dependencies_processed = False
elif (not self.inventory) or self.inventory.inventory_sources.filter(update_on_launch=True).exists():
self.dependencies_processed = False
else:
# No dependencies to process
self.dependencies_processed = True
@property
def event_class(self):
if self.has_unpartitioned_events:
@@ -645,8 +657,7 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
raise ParseError(_('{status_value} is not a valid status option.').format(status_value=status))
return self._get_hosts(**kwargs)
@property
def task_impact(self):
def _get_task_impact(self):
if self.launch_type == 'callback':
count_hosts = 2
else:
@@ -744,6 +755,12 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
return "$hidden due to Ansible no_log flag$"
return artifacts
def get_effective_artifacts(self, **kwargs):
"""Return unified job artifacts (from set_stats) to pass downstream in workflows"""
if isinstance(self.artifacts, dict):
return self.artifacts
return {}
@property
def is_container_group_task(self):
return bool(self.instance_group and self.instance_group.is_container_group)
@@ -797,7 +814,8 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
def _get_inventory_hosts(self, only=['name', 'ansible_facts', 'ansible_facts_modified', 'modified', 'inventory_id']):
if not self.inventory:
return []
return self.inventory.hosts.only(*only)
host_queryset = self.inventory.hosts.only(*only)
return self.inventory.get_sliced_hosts(host_queryset, self.job_slice_number, self.job_slice_count)
def start_job_fact_cache(self, destination, modification_times, timeout=None):
self.log_lifecycle("start_job_fact_cache")
@@ -842,7 +860,7 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
continue
host.ansible_facts = ansible_facts
host.ansible_facts_modified = now()
host.save()
host.save(update_fields=['ansible_facts', 'ansible_facts_modified'])
system_tracking_logger.info(
'New fact for inventory {} host {}'.format(smart_str(host.inventory.name), smart_str(host.name)),
extra=dict(
@@ -1208,6 +1226,9 @@ class SystemJob(UnifiedJob, SystemJobOptions, JobNotificationMixin):
extra_vars_dict = VarsDictProperty('extra_vars', True)
def _set_default_dependencies_processed(self):
self.dependencies_processed = True
@classmethod
def _get_parent_field_name(cls):
return 'system_job_template'
@@ -1233,8 +1254,7 @@ class SystemJob(UnifiedJob, SystemJobOptions, JobNotificationMixin):
return UnpartitionedSystemJobEvent
return SystemJobEvent
@property
def task_impact(self):
def _get_task_impact(self):
return 5
@property

View File

@@ -407,41 +407,59 @@ class TaskManagerUnifiedJobMixin(models.Model):
def get_jobs_fail_chain(self):
return []
def dependent_jobs_finished(self):
return True
class TaskManagerJobMixin(TaskManagerUnifiedJobMixin):
class Meta:
abstract = True
def get_jobs_fail_chain(self):
return [self.project_update] if self.project_update else []
def dependent_jobs_finished(self):
for j in self.dependent_jobs.all():
if j.status in ['pending', 'waiting', 'running']:
return False
return True
if self.project_update_id:
return [self.project_update]
return []
class TaskManagerUpdateOnLaunchMixin(TaskManagerUnifiedJobMixin):
class Meta:
abstract = True
def get_jobs_fail_chain(self):
return list(self.dependent_jobs.all())
class TaskManagerProjectUpdateMixin(TaskManagerUpdateOnLaunchMixin):
class Meta:
abstract = True
def get_jobs_fail_chain(self):
# project update can be a dependency of an inventory update, in which
# case we need to fail the job that may have spawned the inventory
# update.
# The inventory update will fail, but since it is not running it will
# not cascade fail to the job from the errback logic in apply_async. As
# such we should capture it here.
blocked_jobs = list(self.unifiedjob_blocked_jobs.all().prefetch_related("unifiedjob_blocked_jobs"))
other_tasks = []
for b in blocked_jobs:
other_tasks += list(b.unifiedjob_blocked_jobs.all())
return blocked_jobs + other_tasks
class TaskManagerInventoryUpdateMixin(TaskManagerUpdateOnLaunchMixin):
class Meta:
abstract = True
def get_jobs_fail_chain(self):
blocked_jobs = list(self.unifiedjob_blocked_jobs.all())
other_updates = []
if blocked_jobs:
# blocked_jobs[0] is just a reference to a job that depends on this
# inventory update.
# We can look at the dependencies of this blocked job to find other
# inventory sources that are safe to fail.
# Since the dependencies could also include project updates,
# we need to check for type.
for dep in blocked_jobs[0].dependent_jobs.all():
if type(dep) is type(self) and dep.id != self.id:
other_updates.append(dep)
return blocked_jobs + other_updates
class ExecutionEnvironmentMixin(models.Model):
class Meta:

View File

@@ -408,6 +408,7 @@ class JobNotificationMixin(object):
'inventory': 'Stub Inventory',
'id': 42,
'hosts': {},
'extra_vars': {},
'friendly_name': 'Job',
'finished': False,
'credential': 'Stub credential',
@@ -421,21 +422,8 @@ class JobNotificationMixin(object):
The context will contain allowed content retrieved from a serialized job object
(see JobNotificationMixin.JOB_FIELDS_ALLOWED_LIST the job's friendly name,
and a url to the job run."""
job_context = {'host_status_counts': {}}
summary = None
try:
has_event_property = any([f for f in self.event_class._meta.fields if f.name == 'event'])
except NotImplementedError:
has_event_property = False
if has_event_property:
qs = self.get_event_queryset()
if qs:
event = qs.only('event_data').filter(event='playbook_on_stats').first()
if event:
summary = event.get_host_status_counts()
job_context['host_status_counts'] = summary
context = {
'job': job_context,
'job': {'host_status_counts': self.host_status_counts},
'job_friendly_name': self.get_notification_friendly_name(),
'url': self.get_ui_url(),
'job_metadata': json.dumps(self.notification_data(), ensure_ascii=False, indent=4),

View File

@@ -114,13 +114,6 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi
def _get_related_jobs(self):
return UnifiedJob.objects.non_polymorphic().filter(organization=self)
def create_default_galaxy_credential(self):
from awx.main.models import Credential
public_galaxy_credential = Credential.objects.filter(managed=True, name='Ansible Galaxy').first()
if public_galaxy_credential is not None and public_galaxy_credential not in self.galaxy_credentials.all():
self.galaxy_credentials.add(public_galaxy_credential)
class OrganizationGalaxyCredentialMembership(models.Model):

View File

@@ -284,6 +284,17 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
help_text=_('Allow changing the SCM branch or revision in a job template ' 'that uses this project.'),
)
# credential (keys) used to validate content signature
signature_validation_credential = models.ForeignKey(
'Credential',
related_name='%(class)ss_signature_validation',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
help_text=_('An optional credential used for validating files in the project against unexpected changes.'),
)
scm_revision = models.CharField(
max_length=1024,
blank=True,
@@ -354,7 +365,7 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
# If update_fields has been specified, add our field names to it,
# if it hasn't been specified, then we're just doing a normal save.
update_fields = kwargs.get('update_fields', [])
skip_update = bool(kwargs.pop('skip_update', False))
self._skip_update = bool(kwargs.pop('skip_update', False))
# Create auto-generated local path if project uses SCM.
if self.pk and self.scm_type and not self.local_path.startswith('_'):
slug_name = slugify(str(self.name)).replace(u'-', u'_')
@@ -372,14 +383,16 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
from awx.main.signals import disable_activity_stream
with disable_activity_stream():
self.save(update_fields=update_fields)
self.save(update_fields=update_fields, skip_update=self._skip_update)
# If we just created a new project with SCM, start the initial update.
# also update if certain fields have changed
relevant_change = any(pre_save_vals.get(fd_name, None) != self._prior_values_store.get(fd_name, None) for fd_name in self.FIELDS_TRIGGER_UPDATE)
if (relevant_change or new_instance) and (not skip_update) and self.scm_type:
if (relevant_change or new_instance) and (not self._skip_update) and self.scm_type:
self.update()
def _get_current_status(self):
if getattr(self, '_skip_update', False):
return self.status
if self.scm_type:
if self.current_job and self.current_job.status:
return self.current_job.status
@@ -511,6 +524,9 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin, TaskManage
help_text=_('The SCM Revision discovered by this update for the given project and branch.'),
)
def _set_default_dependencies_processed(self):
self.dependencies_processed = True
def _get_parent_field_name(self):
return 'project'
@@ -558,8 +574,7 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin, TaskManage
return UnpartitionedProjectUpdateEvent
return ProjectUpdateEvent
@property
def task_impact(self):
def _get_task_impact(self):
return 0 if self.job_type == 'run' else 1
@property
@@ -616,6 +631,10 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin, TaskManage
added_update_fields = []
if not self.job_tags:
job_tags = ['update_{}'.format(self.scm_type), 'install_roles', 'install_collections']
if self.project.signature_validation_credential is not None:
credential_type = self.project.signature_validation_credential.credential_type.namespace
job_tags.append(f'validation_{credential_type}')
job_tags.append('validation_checksum_manifest')
self.job_tags = ','.join(job_tags)
added_update_fields.append('job_tags')
if self.scm_delete_on_update and 'delete' not in self.job_tags and self.job_type == 'check':

View File

@@ -81,32 +81,41 @@ class Schedule(PrimordialModel, LaunchTimeConfig):
dtend = models.DateTimeField(
null=True, default=None, editable=False, help_text=_("The last occurrence of the schedule occurs before this time, aftewards the schedule expires.")
)
rrule = models.CharField(max_length=255, help_text=_("A value representing the schedules iCal recurrence rule."))
rrule = models.TextField(help_text=_("A value representing the schedules iCal recurrence rule."))
next_run = models.DateTimeField(null=True, default=None, editable=False, help_text=_("The next time that the scheduled action will run."))
@classmethod
def get_zoneinfo(self):
def get_zoneinfo(cls):
return sorted(get_zonefile_instance().zones)
@classmethod
def get_zoneinfo_links(cls):
return_val = {}
zone_instance = get_zonefile_instance()
for zone_name in zone_instance.zones:
if str(zone_name) != str(zone_instance.zones[zone_name]._filename):
return_val[zone_name] = zone_instance.zones[zone_name]._filename
return return_val
@property
def timezone(self):
utc = tzutc()
# All rules in a ruleset will have the same dtstart so we can just take the first rule
tzinfo = Schedule.rrulestr(self.rrule)._rrule[0]._dtstart.tzinfo
if tzinfo is utc:
return 'UTC'
all_zones = Schedule.get_zoneinfo()
all_zones.sort(key=lambda x: -len(x))
for r in Schedule.rrulestr(self.rrule)._rrule:
if r._dtstart:
tzinfo = r._dtstart.tzinfo
if tzinfo is utc:
return 'UTC'
fname = getattr(tzinfo, '_filename', None)
if fname:
for zone in all_zones:
if fname.endswith(zone):
return zone
fname = getattr(tzinfo, '_filename', None)
if fname:
for zone in all_zones:
if fname.endswith(zone):
return zone
logger.warning('Could not detect valid zoneinfo for {}'.format(self.rrule))
return ''
@property
# TODO: How would we handle multiple until parameters? The UI is currently using this on the edit screen of a schedule
def until(self):
# The UNTIL= datestamp (if any) coerced from UTC to the local naive time
# of the DTSTART
@@ -134,34 +143,48 @@ class Schedule(PrimordialModel, LaunchTimeConfig):
# timezone (America/New_York), and so we'll coerce to UTC _for you_
# automatically.
#
if 'until=' in rrule.lower():
# if DTSTART;TZID= is used, coerce "naive" UNTIL values
# to the proper UTC date
match_until = re.match(r".*?(?P<until>UNTIL\=[0-9]+T[0-9]+)(?P<utcflag>Z?)", rrule)
if not len(match_until.group('utcflag')):
# rrule = DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000
# Find the UNTIL=N part of the string
# naive_until = UNTIL=20200601T170000
naive_until = match_until.group('until')
# Find the DTSTART rule or raise an error, its usually the first rule but that is not strictly enforced
start_date_rule = re.sub('^.*(DTSTART[^\s]+)\s.*$', r'\1', rrule)
if not start_date_rule:
raise ValueError('A DTSTART field needs to be in the rrule')
# What is the DTSTART timezone for:
# DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000Z
# local_tz = tzfile('/usr/share/zoneinfo/America/New_York')
local_tz = dateutil.rrule.rrulestr(rrule.replace(naive_until, naive_until + 'Z'), tzinfos=UTC_TIMEZONES)._dtstart.tzinfo
rules = re.split(r'\s+', rrule)
for index in range(0, len(rules)):
rule = rules[index]
if 'until=' in rule.lower():
# if DTSTART;TZID= is used, coerce "naive" UNTIL values
# to the proper UTC date
match_until = re.match(r".*?(?P<until>UNTIL\=[0-9]+T[0-9]+)(?P<utcflag>Z?)", rule)
if not len(match_until.group('utcflag')):
# rule = DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000
# Make a datetime object with tzinfo=<the DTSTART timezone>
# localized_until = datetime.datetime(2020, 6, 1, 17, 0, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York'))
localized_until = make_aware(datetime.datetime.strptime(re.sub('^UNTIL=', '', naive_until), "%Y%m%dT%H%M%S"), local_tz)
# Find the UNTIL=N part of the string
# naive_until = UNTIL=20200601T170000
naive_until = match_until.group('until')
# Coerce the datetime to UTC and format it as a string w/ Zulu format
# utc_until = UNTIL=20200601T220000Z
utc_until = 'UNTIL=' + localized_until.astimezone(pytz.utc).strftime('%Y%m%dT%H%M%SZ')
# What is the DTSTART timezone for:
# DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000Z
# local_tz = tzfile('/usr/share/zoneinfo/America/New_York')
# We are going to construct a 'dummy' rule for parsing which will include the DTSTART and the rest of the rule
temp_rule = "{} {}".format(start_date_rule, rule.replace(naive_until, naive_until + 'Z'))
# If the rule is an EX rule we have to add an RRULE to it because an EX rule alone will not manifest into a ruleset
if rule.lower().startswith('ex'):
temp_rule = "{} {}".format(temp_rule, 'RRULE:FREQ=MINUTELY;INTERVAL=1;UNTIL=20380601T170000Z')
local_tz = dateutil.rrule.rrulestr(temp_rule, tzinfos=UTC_TIMEZONES, **{'forceset': True})._rrule[0]._dtstart.tzinfo
# rrule was: DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000
# rrule is now: DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T220000Z
rrule = rrule.replace(naive_until, utc_until)
return rrule
# Make a datetime object with tzinfo=<the DTSTART timezone>
# localized_until = datetime.datetime(2020, 6, 1, 17, 0, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York'))
localized_until = make_aware(datetime.datetime.strptime(re.sub('^UNTIL=', '', naive_until), "%Y%m%dT%H%M%S"), local_tz)
# Coerce the datetime to UTC and format it as a string w/ Zulu format
# utc_until = UNTIL=20200601T220000Z
utc_until = 'UNTIL=' + localized_until.astimezone(pytz.utc).strftime('%Y%m%dT%H%M%SZ')
# rule was: DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000
# rule is now: DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T220000Z
rules[index] = rule.replace(naive_until, utc_until)
return " ".join(rules)
@classmethod
def rrulestr(cls, rrule, fast_forward=True, **kwargs):
@@ -176,20 +199,28 @@ class Schedule(PrimordialModel, LaunchTimeConfig):
if r._dtstart and r._dtstart.tzinfo is None:
raise ValueError('A valid TZID must be provided (e.g., America/New_York)')
if fast_forward and ('MINUTELY' in rrule or 'HOURLY' in rrule) and 'COUNT=' not in rrule:
# Fast forward is a way for us to limit the number of events in the rruleset
# If we are fastforwading and we don't have a count limited rule that is minutely or hourley
# We will modify the start date of the rule to last week to prevent a large number of entries
if fast_forward:
try:
# All rules in a ruleset will have the same dtstart value
# so lets compare the first event to now to see if its > 7 days old
first_event = x[0]
# If the first event was over a week ago...
if (now() - first_event).days > 7:
# hourly/minutely rrules with far-past DTSTART values
# are *really* slow to precompute
# start *from* one week ago to speed things up drastically
dtstart = x._rrule[0]._dtstart.strftime(':%Y%m%dT')
new_start = (now() - datetime.timedelta(days=7)).strftime(':%Y%m%dT')
new_rrule = rrule.replace(dtstart, new_start)
return Schedule.rrulestr(new_rrule, fast_forward=False)
for rule in x._rrule:
# If any rule has a minutely or hourly rule without a count...
if rule._freq in [dateutil.rrule.MINUTELY, dateutil.rrule.HOURLY] and not rule._count:
# hourly/minutely rrules with far-past DTSTART values
# are *really* slow to precompute
# start *from* one week ago to speed things up drastically
new_start = (now() - datetime.timedelta(days=7)).strftime('%Y%m%d')
# Now we want to repalce the DTSTART:<value>T with the new date (which includes the T)
new_rrule = re.sub('(DTSTART[^:]*):[^T]+T', r'\1:{0}T'.format(new_start), rrule)
return Schedule.rrulestr(new_rrule, fast_forward=False)
except IndexError:
pass
return x
def __str__(self):
@@ -206,6 +237,22 @@ class Schedule(PrimordialModel, LaunchTimeConfig):
job_kwargs['_eager_fields'] = {'launch_type': 'scheduled', 'schedule': self}
return job_kwargs
def get_end_date(ruleset):
# if we have a complex ruleset with a lot of options getting the last index of the ruleset can take some time
# And a ruleset without a count/until can come back as datetime.datetime(9999, 12, 31, 15, 0, tzinfo=tzfile('US/Eastern'))
# So we are going to do a quick scan to make sure we would have an end date
for a_rule in ruleset._rrule:
# if this rule does not have until or count in it then we have no end date
if not a_rule._until and not a_rule._count:
return None
# If we made it this far we should have an end date and can ask the ruleset what the last date is
# However, if the until/count is before dtstart we will get an IndexError when trying to get [-1]
try:
return ruleset[-1].astimezone(pytz.utc)
except IndexError:
return None
def update_computed_fields_no_save(self):
affects_fields = ['next_run', 'dtstart', 'dtend']
starting_values = {}
@@ -229,12 +276,7 @@ class Schedule(PrimordialModel, LaunchTimeConfig):
self.dtstart = future_rs[0].astimezone(pytz.utc)
except IndexError:
self.dtstart = None
self.dtend = None
if 'until' in self.rrule.lower() or 'count' in self.rrule.lower():
try:
self.dtend = future_rs[-1].astimezone(pytz.utc)
except IndexError:
self.dtend = None
self.dtend = Schedule.get_end_date(future_rs)
changed = any(getattr(self, field_name) != starting_values[field_name] for field_name in affects_fields)
return changed

View File

@@ -45,7 +45,8 @@ from awx.main.utils.common import (
get_type_for_model,
parse_yaml_or_json,
getattr_dne,
schedule_task_manager,
ScheduleDependencyManager,
ScheduleTaskManager,
get_event_partition_epoch,
get_capacity_type,
)
@@ -381,6 +382,11 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEn
unified_job.survey_passwords = new_job_passwords
kwargs['survey_passwords'] = new_job_passwords # saved in config object for relaunch
unified_job.preferred_instance_groups_cache = unified_job._get_preferred_instance_group_cache()
unified_job._set_default_dependencies_processed()
unified_job.task_impact = unified_job._get_task_impact()
from awx.main.signals import disable_activity_stream, activity_stream_create
with disable_activity_stream():
@@ -533,7 +539,7 @@ class UnifiedJob(
('workflow', _('Workflow')), # Job was started from a workflow job.
('webhook', _('Webhook')), # Job was started from a webhook event.
('sync', _('Sync')), # Job was started from a project sync.
('scm', _('SCM Update')), # Job was created as an Inventory SCM sync.
('scm', _('SCM Update')), # (deprecated) Job was created as an Inventory SCM sync.
]
PASSWORD_FIELDS = ('start_args',)
@@ -575,7 +581,8 @@ class UnifiedJob(
dependent_jobs = models.ManyToManyField(
'self',
editable=False,
related_name='%(class)s_blocked_jobs+',
related_name='%(class)s_blocked_jobs',
symmetrical=False,
)
execution_node = models.TextField(
blank=True,
@@ -692,6 +699,14 @@ class UnifiedJob(
on_delete=polymorphic.SET_NULL,
help_text=_('The Instance group the job was run under'),
)
preferred_instance_groups_cache = models.JSONField(
blank=True,
null=True,
default=None,
editable=False,
help_text=_("A cached list with pk values from preferred instance groups."),
)
task_impact = models.PositiveIntegerField(default=0, editable=False, help_text=_("Number of forks an instance consumes when running this job."))
organization = models.ForeignKey(
'Organization',
blank=True,
@@ -717,6 +732,13 @@ class UnifiedJob(
editable=False,
help_text=_("The version of Ansible Core installed in the execution environment."),
)
host_status_counts = models.JSONField(
blank=True,
null=True,
default=None,
editable=False,
help_text=_("Playbook stats from the Ansible playbook_on_stats event."),
)
work_unit_id = models.CharField(
max_length=255, blank=True, default=None, editable=False, null=True, help_text=_("The Receptor work unit ID associated with this job.")
)
@@ -746,6 +768,9 @@ class UnifiedJob(
def _get_parent_field_name(self):
return 'unified_job_template' # Override in subclasses.
def _get_preferred_instance_group_cache(self):
return [ig.pk for ig in self.preferred_instance_groups]
@classmethod
def _get_unified_job_template_class(cls):
"""
@@ -800,6 +825,9 @@ class UnifiedJob(
update_fields = self._update_parent_instance_no_save(parent_instance)
parent_instance.save(update_fields=update_fields)
def _set_default_dependencies_processed(self):
pass
def save(self, *args, **kwargs):
"""Save the job, with current status, to the database.
Ensure that all data is consistent before doing so.
@@ -813,7 +841,8 @@ class UnifiedJob(
# If this job already exists in the database, retrieve a copy of
# the job in its prior state.
if self.pk:
# If update_fields are given without status, then that indicates no change
if self.pk and ((not update_fields) or ('status' in update_fields)):
self_before = self.__class__.objects.get(pk=self.pk)
if self_before.status != self.status:
status_before = self_before.status
@@ -1018,7 +1047,6 @@ class UnifiedJob(
event_qs = self.get_event_queryset()
except NotImplementedError:
return True # Model without events, such as WFJT
self.log_lifecycle("event_processing_finished")
return self.emitted_events == event_qs.count()
def result_stdout_raw_handle(self, enforce_max_bytes=True):
@@ -1196,6 +1224,10 @@ class UnifiedJob(
pass
return None
def get_effective_artifacts(self, **kwargs):
"""Return unified job artifacts (from set_stats) to pass downstream in workflows"""
return {}
def get_passwords_needed_to_start(self):
return []
@@ -1229,9 +1261,8 @@ class UnifiedJob(
except JobLaunchConfig.DoesNotExist:
return False
@property
def task_impact(self):
raise NotImplementedError # Implement in subclass.
def _get_task_impact(self):
return self.task_impact # return default, should implement in subclass.
def websocket_emit_data(self):
'''Return extra data that should be included when submitting data to the browser over the websocket connection'''
@@ -1243,7 +1274,7 @@ class UnifiedJob(
def _websocket_emit_status(self, status):
try:
status_data = dict(unified_job_id=self.id, status=status)
if status == 'waiting':
if status == 'running':
if self.instance_group:
status_data['instance_group_name'] = self.instance_group.name
else:
@@ -1346,7 +1377,10 @@ class UnifiedJob(
self.update_fields(start_args=json.dumps(kwargs), status='pending')
self.websocket_emit_status("pending")
schedule_task_manager()
if self.dependencies_processed:
ScheduleTaskManager().schedule()
else:
ScheduleDependencyManager().schedule()
# Each type of unified job has a different Task class; get the
# appropirate one.
@@ -1361,22 +1395,6 @@ class UnifiedJob(
# Done!
return True
@property
def actually_running(self):
# returns True if the job is running in the appropriate dispatcher process
running = False
if all([self.status == 'running', self.celery_task_id, self.execution_node]):
# If the job is marked as running, but the dispatcher
# doesn't know about it (or the dispatcher doesn't reply),
# then cancel the job
timeout = 5
try:
running = self.celery_task_id in ControlDispatcher('dispatcher', self.controller_node or self.execution_node).running(timeout=timeout)
except (socket.timeout, RuntimeError):
logger.error('could not reach dispatcher on {} within {}s'.format(self.execution_node, timeout))
running = False
return running
@property
def can_cancel(self):
return bool(self.status in CAN_CANCEL)
@@ -1386,27 +1404,61 @@ class UnifiedJob(
return 'Previous Task Canceled: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % (self.model_to_str(), self.name, self.id)
return None
def fallback_cancel(self):
if not self.celery_task_id:
self.refresh_from_db(fields=['celery_task_id'])
self.cancel_dispatcher_process()
def cancel_dispatcher_process(self):
"""Returns True if dispatcher running this job acknowledged request and sent SIGTERM"""
if not self.celery_task_id:
return
canceled = []
try:
# Use control and reply mechanism to cancel and obtain confirmation
timeout = 5
canceled = ControlDispatcher('dispatcher', self.controller_node).cancel([self.celery_task_id])
except socket.timeout:
logger.error(f'could not reach dispatcher on {self.controller_node} within {timeout}s')
except Exception:
logger.exception("error encountered when checking task status")
return bool(self.celery_task_id in canceled) # True or False, whether confirmation was obtained
def cancel(self, job_explanation=None, is_chain=False):
if self.can_cancel:
if not is_chain:
for x in self.get_jobs_fail_chain():
x.cancel(job_explanation=self._build_job_explanation(), is_chain=True)
cancel_fields = []
if not self.cancel_flag:
self.cancel_flag = True
self.start_args = '' # blank field to remove encrypted passwords
cancel_fields = ['cancel_flag', 'start_args']
if self.status in ('pending', 'waiting', 'new'):
self.status = 'canceled'
cancel_fields.append('status')
if self.status == 'running' and not self.actually_running:
self.status = 'canceled'
cancel_fields.append('status')
cancel_fields.extend(['cancel_flag', 'start_args'])
connection.on_commit(lambda: self.websocket_emit_status("canceled"))
if job_explanation is not None:
self.job_explanation = job_explanation
cancel_fields.append('job_explanation')
self.save(update_fields=cancel_fields)
self.websocket_emit_status("canceled")
controller_notified = False
if self.celery_task_id:
controller_notified = self.cancel_dispatcher_process()
else:
# Avoid race condition where we have stale model from pending state but job has already started,
# its checking signal but not cancel_flag, so re-send signal after this database commit
connection.on_commit(self.fallback_cancel)
# If a SIGTERM signal was sent to the control process, and acked by the dispatcher
# then we want to let its own cleanup change status, otherwise change status now
if not controller_notified:
if self.status != 'canceled':
self.status = 'canceled'
cancel_fields.append('status')
self.save(update_fields=cancel_fields)
return self.cancel_flag
@property
@@ -1503,8 +1555,8 @@ class UnifiedJob(
'state': state,
'work_unit_id': self.work_unit_id,
}
if self.unified_job_template:
extra["template_name"] = self.unified_job_template.name
if self.name:
extra["task_name"] = self.name
if state == "blocked" and blocked_by:
blocked_by_msg = f"{blocked_by._meta.model_name}-{blocked_by.id}"
msg = f"{self._meta.model_name}-{self.id} blocked by {blocked_by_msg}"
@@ -1516,7 +1568,7 @@ class UnifiedJob(
extra["controller_node"] = self.controller_node or "NOT_SET"
elif state == "execution_node_chosen":
extra["execution_node"] = self.execution_node or "NOT_SET"
logger_job_lifecycle.debug(msg, extra=extra)
logger_job_lifecycle.info(msg, extra=extra)
@property
def launched_by(self):

View File

@@ -13,6 +13,7 @@ from django.db import connection, models
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist
from django.utils.timezone import now, timedelta
# from django import settings as tower_settings
@@ -40,7 +41,7 @@ from awx.main.models.mixins import (
from awx.main.models.jobs import LaunchTimeConfigBase, LaunchTimeConfig, JobTemplate
from awx.main.models.credential import Credential
from awx.main.redact import REPLACE_STR
from awx.main.utils import schedule_task_manager
from awx.main.utils import ScheduleWorkflowManager
__all__ = [
@@ -318,8 +319,8 @@ class WorkflowJobNode(WorkflowNodeBase):
for parent_node in self.get_parent_nodes():
is_root_node = False
aa_dict.update(parent_node.ancestor_artifacts)
if parent_node.job and hasattr(parent_node.job, 'artifacts'):
aa_dict.update(parent_node.job.artifacts)
if parent_node.job:
aa_dict.update(parent_node.job.get_effective_artifacts(parents_set=set([self.workflow_job_id])))
if aa_dict and not is_root_node:
self.ancestor_artifacts = aa_dict
self.save(update_fields=['ancestor_artifacts'])
@@ -622,6 +623,9 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio
)
is_sliced_job = models.BooleanField(default=False)
def _set_default_dependencies_processed(self):
self.dependencies_processed = True
@property
def workflow_nodes(self):
return self.workflow_job_nodes
@@ -659,10 +663,16 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio
node_job_description = 'job #{0}, "{1}", which finished with status {2}.'.format(node.job.id, node.job.name, node.job.status)
str_arr.append("- node #{0} spawns {1}".format(node.id, node_job_description))
result['body'] = '\n'.join(str_arr)
result.update(
dict(
inventory=self.inventory.name if self.inventory else None,
limit=self.limit,
extra_vars=self.display_extra_vars(),
)
)
return result
@property
def task_impact(self):
def _get_task_impact(self):
return 0
def get_ancestor_workflows(self):
@@ -682,6 +692,27 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio
wj = wj.get_workflow_job()
return ancestors
def get_effective_artifacts(self, **kwargs):
"""
For downstream jobs of a workflow nested inside of a workflow,
we send aggregated artifacts from the nodes inside of the nested workflow
"""
artifacts = {}
job_queryset = (
UnifiedJob.objects.filter(unified_job_node__workflow_job=self)
.defer('job_args', 'job_cwd', 'start_args', 'result_traceback')
.order_by('finished', 'id')
.filter(status__in=['successful', 'failed'])
.iterator()
)
parents_set = kwargs.get('parents_set', set())
new_parents_set = parents_set | {self.id}
for job in job_queryset:
if job.id in parents_set:
continue
artifacts.update(job.get_effective_artifacts(parents_set=new_parents_set))
return artifacts
def get_notification_templates(self):
return self.workflow_job_template.notification_templates
@@ -692,11 +723,10 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio
def preferred_instance_groups(self):
return []
@property
def actually_running(self):
def cancel_dispatcher_process(self):
# WorkflowJobs don't _actually_ run anything in the dispatcher, so
# there's no point in asking the dispatcher if it knows about this task
return self.status == 'running'
return True
class WorkflowApprovalTemplate(UnifiedJobTemplate, RelatedJobsMixin):
@@ -755,6 +785,12 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin):
default=0,
help_text=_("The amount of time (in seconds) before the approval node expires and fails."),
)
expires = models.DateTimeField(
default=None,
null=True,
editable=False,
help_text=_("The time this approval will expire. This is the created time plus timeout, used for filtering."),
)
timed_out = models.BooleanField(default=False, help_text=_("Shows when an approval node (with a timeout assigned to it) has timed out."))
approved_or_denied_by = models.ForeignKey(
'auth.User',
@@ -765,6 +801,9 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin):
on_delete=models.SET_NULL,
)
def _set_default_dependencies_processed(self):
self.dependencies_processed = True
@classmethod
def _get_unified_job_template_class(cls):
return WorkflowApprovalTemplate
@@ -782,13 +821,32 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin):
def _get_parent_field_name(self):
return 'workflow_approval_template'
def save(self, *args, **kwargs):
update_fields = list(kwargs.get('update_fields', []))
if self.timeout != 0 and ((not self.pk) or (not update_fields) or ('timeout' in update_fields)):
if not self.created: # on creation, created will be set by parent class, so we fudge it here
created = now()
else:
created = self.created
new_expires = created + timedelta(seconds=self.timeout)
if new_expires != self.expires:
self.expires = new_expires
if update_fields and 'expires' not in update_fields:
update_fields.append('expires')
elif self.timeout == 0 and ((not update_fields) or ('timeout' in update_fields)):
if self.expires:
self.expires = None
if update_fields and 'expires' not in update_fields:
update_fields.append('expires')
super(WorkflowApproval, self).save(*args, **kwargs)
def approve(self, request=None):
self.status = 'successful'
self.approved_or_denied_by = get_current_user()
self.save()
self.send_approval_notification('approved')
self.websocket_emit_status(self.status)
schedule_task_manager()
ScheduleWorkflowManager().schedule()
return reverse('api:workflow_approval_approve', kwargs={'pk': self.pk}, request=request)
def deny(self, request=None):
@@ -797,7 +855,7 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin):
self.save()
self.send_approval_notification('denied')
self.websocket_emit_status(self.status)
schedule_task_manager()
ScheduleWorkflowManager().schedule()
return reverse('api:workflow_approval_deny', kwargs={'pk': self.pk}, request=request)
def signal_start(self, **kwargs):
@@ -885,3 +943,12 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin):
@property
def workflow_job(self):
return self.unified_job_node.workflow_job
def notification_data(self):
result = super(WorkflowApproval, self).notification_data()
result.update(
dict(
extra_vars=self.workflow_job.display_extra_vars(),
)
)
return result

View File

@@ -8,7 +8,6 @@ import redis
# Django
from django.conf import settings
import awx.main.analytics.subsystem_metrics as s_metrics
__all__ = ['CallbackQueueDispatcher']
@@ -28,7 +27,6 @@ class CallbackQueueDispatcher(object):
self.queue = getattr(settings, 'CALLBACK_QUEUE', '')
self.logger = logging.getLogger('awx.main.queue.CallbackQueueDispatcher')
self.connection = redis.Redis.from_url(settings.BROKER_URL)
self.subsystem_metrics = s_metrics.Metrics()
def dispatch(self, obj):
self.connection.rpush(self.queue, json.dumps(obj, cls=AnsibleJSONEncoder))

View File

@@ -1,6 +1,6 @@
# Copyright (c) 2017 Ansible, Inc.
#
from .task_manager import TaskManager
from .task_manager import TaskManager, DependencyManager, WorkflowManager
__all__ = ['TaskManager']
__all__ = ['TaskManager', 'DependencyManager', 'WorkflowManager']

View File

@@ -7,6 +7,11 @@ from awx.main.models import (
WorkflowJob,
)
import logging
logger = logging.getLogger('awx.main.scheduler.dependency_graph')
class DependencyGraph(object):
PROJECT_UPDATES = 'project_updates'
@@ -26,7 +31,7 @@ class DependencyGraph(object):
# The reason for tracking both inventory and inventory sources:
# Consider InvA, which has two sources, InvSource1, InvSource2.
# JobB might depend on InvA, which launches two updates, one for each source.
# To determine if JobB can run, we can just check InvA, which is marked in
# To determine if JobB can run, we can just check InvA, which is marked in
# INVENTORY_UPDATES, instead of having to check for both entries in
# INVENTORY_SOURCE_UPDATES.
self.data[self.INVENTORY_UPDATES] = {}
@@ -36,6 +41,9 @@ class DependencyGraph(object):
self.data[self.WORKFLOW_JOB_TEMPLATES_JOBS] = {}
def mark_if_no_key(self, job_type, id, job):
if id is None:
logger.warning(f'Null dependency graph key from {job}, could be integrity error or bug, ignoring')
return
# only mark first occurrence of a task. If 10 of JobA are launched
# (concurrent disabled), the dependency graph should return that jobs
# 2 through 10 are blocked by job1
@@ -66,7 +74,10 @@ class DependencyGraph(object):
self.mark_if_no_key(self.JOB_TEMPLATE_JOBS, job.job_template_id, job)
def mark_workflow_job(self, job):
self.mark_if_no_key(self.WORKFLOW_JOB_TEMPLATES_JOBS, job.workflow_job_template_id, job)
if job.workflow_job_template_id:
self.mark_if_no_key(self.WORKFLOW_JOB_TEMPLATES_JOBS, job.workflow_job_template_id, job)
elif job.unified_job_template_id: # for sliced jobs
self.mark_if_no_key(self.WORKFLOW_JOB_TEMPLATES_JOBS, job.unified_job_template_id, job)
def project_update_blocked_by(self, job):
return self.get_item(self.PROJECT_UPDATES, job.project_id)
@@ -85,7 +96,13 @@ class DependencyGraph(object):
def workflow_job_blocked_by(self, job):
if job.allow_simultaneous is False:
return self.get_item(self.WORKFLOW_JOB_TEMPLATES_JOBS, job.workflow_job_template_id)
if job.workflow_job_template_id:
return self.get_item(self.WORKFLOW_JOB_TEMPLATES_JOBS, job.workflow_job_template_id)
elif job.unified_job_template_id:
# Sliced jobs can be either Job or WorkflowJob type, and either should block a sliced WorkflowJob
return self.get_item(self.WORKFLOW_JOB_TEMPLATES_JOBS, job.unified_job_template_id) or self.get_item(
self.JOB_TEMPLATE_JOBS, job.unified_job_template_id
)
return None
def system_job_blocked_by(self, job):

View File

@@ -6,176 +6,152 @@ from datetime import timedelta
import logging
import uuid
import json
import time
import sys
import signal
# Django
from django.db import transaction, connection
from django.db import transaction
from django.utils.translation import gettext_lazy as _, gettext_noop
from django.utils.timezone import now as tz_now
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
# AWX
from awx.main.dispatch.reaper import reap_job
from awx.main.models import (
AdHocCommand,
Instance,
InventorySource,
InventoryUpdate,
Job,
Project,
ProjectUpdate,
SystemJob,
UnifiedJob,
WorkflowApproval,
WorkflowJob,
WorkflowJobNode,
WorkflowJobTemplate,
)
from awx.main.scheduler.dag_workflow import WorkflowDAG
from awx.main.utils.pglock import advisory_lock
from awx.main.utils import get_type_for_model, task_manager_bulk_reschedule, schedule_task_manager
from awx.main.utils.common import create_partition
from awx.main.utils import (
get_type_for_model,
ScheduleTaskManager,
ScheduleWorkflowManager,
)
from awx.main.utils.common import task_manager_bulk_reschedule
from awx.main.signals import disable_activity_stream
from awx.main.constants import ACTIVE_STATES
from awx.main.scheduler.dependency_graph import DependencyGraph
from awx.main.scheduler.task_manager_models import TaskManagerInstances
from awx.main.scheduler.task_manager_models import TaskManagerInstanceGroups
import awx.main.analytics.subsystem_metrics as s_metrics
from awx.main.utils import decrypt_field
logger = logging.getLogger('awx.main.scheduler')
class TaskManager:
def __init__(self):
"""
Do NOT put database queries or other potentially expensive operations
in the task manager init. The task manager object is created every time a
job is created, transitions state, and every 30 seconds on each tower node.
More often then not, the object is destroyed quickly because the NOOP case is hit.
def timeit(func):
def inner(*args, **kwargs):
t_now = time.perf_counter()
result = func(*args, **kwargs)
dur = time.perf_counter() - t_now
args[0].subsystem_metrics.inc(f"{args[0].prefix}_{func.__name__}_seconds", dur)
return result
The NOOP case is short-circuit logic. If the task manager realizes that another instance
of the task manager is already running, then it short-circuits and decides not to run.
"""
# start task limit indicates how many pending jobs can be started on this
# .schedule() run. Starting jobs is expensive, and there is code in place to reap
# the task manager after 5 minutes. At scale, the task manager can easily take more than
# 5 minutes to start pending jobs. If this limit is reached, pending jobs
# will no longer be started and will be started on the next task manager cycle.
return inner
class TaskBase:
def __init__(self, prefix=""):
self.prefix = prefix
# initialize each metric to 0 and force metric_has_changed to true. This
# ensures each task manager metric will be overridden when pipe_execute
# is called later.
self.subsystem_metrics = s_metrics.Metrics(auto_pipe_execute=False)
self.start_time = time.time()
self.start_task_limit = settings.START_TASK_LIMIT
self.time_delta_job_explanation = timedelta(seconds=30)
for m in self.subsystem_metrics.METRICS:
if m.startswith(self.prefix):
self.subsystem_metrics.set(m, 0)
def after_lock_init(self, all_sorted_tasks):
"""
Init AFTER we know this instance of the task manager will run because the lock is acquired.
"""
self.dependency_graph = DependencyGraph()
self.instances = TaskManagerInstances(all_sorted_tasks)
self.instance_groups = TaskManagerInstanceGroups(instances_by_hostname=self.instances)
self.controlplane_ig = self.instance_groups.controlplane_ig
def timed_out(self):
"""Return True/False if we have met or exceeded the timeout for the task manager."""
elapsed = time.time() - self.start_time
if elapsed >= settings.TASK_MANAGER_TIMEOUT:
logger.warning(f"{self.prefix} manager has run for {elapsed} which is greater than TASK_MANAGER_TIMEOUT of {settings.TASK_MANAGER_TIMEOUT}.")
return True
return False
def job_blocked_by(self, task):
# TODO: I'm not happy with this, I think blocking behavior should be decided outside of the dependency graph
# in the old task manager this was handled as a method on each task object outside of the graph and
# probably has the side effect of cutting down *a lot* of the logic from this task manager class
blocked_by = self.dependency_graph.task_blocked_by(task)
if blocked_by:
return blocked_by
if not task.dependent_jobs_finished():
blocked_by = task.dependent_jobs.first()
if blocked_by:
return blocked_by
return None
def get_tasks(self, status_list=('pending', 'waiting', 'running')):
jobs = [j for j in Job.objects.filter(status__in=status_list).prefetch_related('instance_group')]
inventory_updates_qs = (
InventoryUpdate.objects.filter(status__in=status_list).exclude(source='file').prefetch_related('inventory_source', 'instance_group')
@timeit
def get_tasks(self, filter_args):
wf_approval_ctype_id = ContentType.objects.get_for_model(WorkflowApproval).id
qs = (
UnifiedJob.objects.filter(**filter_args)
.exclude(launch_type='sync')
.exclude(polymorphic_ctype_id=wf_approval_ctype_id)
.order_by('created')
.prefetch_related('dependent_jobs')
)
inventory_updates = [i for i in inventory_updates_qs]
# Notice the job_type='check': we want to prevent implicit project updates from blocking our jobs.
project_updates = [p for p in ProjectUpdate.objects.filter(status__in=status_list, job_type='check').prefetch_related('instance_group')]
system_jobs = [s for s in SystemJob.objects.filter(status__in=status_list).prefetch_related('instance_group')]
ad_hoc_commands = [a for a in AdHocCommand.objects.filter(status__in=status_list).prefetch_related('instance_group')]
workflow_jobs = [w for w in WorkflowJob.objects.filter(status__in=status_list)]
all_tasks = sorted(jobs + project_updates + inventory_updates + system_jobs + ad_hoc_commands + workflow_jobs, key=lambda task: task.created)
return all_tasks
self.all_tasks = [t for t in qs]
def get_running_workflow_jobs(self):
graph_workflow_jobs = [wf for wf in WorkflowJob.objects.filter(status='running')]
return graph_workflow_jobs
def get_inventory_source_tasks(self, all_sorted_tasks):
inventory_ids = set()
for task in all_sorted_tasks:
if isinstance(task, Job):
inventory_ids.add(task.inventory_id)
return [invsrc for invsrc in InventorySource.objects.filter(inventory_id__in=inventory_ids, update_on_launch=True)]
def spawn_workflow_graph_jobs(self, workflow_jobs):
for workflow_job in workflow_jobs:
if workflow_job.cancel_flag:
logger.debug('Not spawning jobs for %s because it is pending cancelation.', workflow_job.log_format)
continue
dag = WorkflowDAG(workflow_job)
spawn_nodes = dag.bfs_nodes_to_run()
if spawn_nodes:
logger.debug('Spawning jobs for %s', workflow_job.log_format)
def record_aggregate_metrics(self, *args):
if not settings.IS_TESTING():
# increment task_manager_schedule_calls regardless if the other
# metrics are recorded
s_metrics.Metrics(auto_pipe_execute=True).inc(f"{self.prefix}__schedule_calls", 1)
# Only record metrics if the last time recording was more
# than SUBSYSTEM_METRICS_TASK_MANAGER_RECORD_INTERVAL ago.
# Prevents a short-duration task manager that runs directly after a
# long task manager to override useful metrics.
current_time = time.time()
time_last_recorded = current_time - self.subsystem_metrics.decode(f"{self.prefix}_recorded_timestamp")
if time_last_recorded > settings.SUBSYSTEM_METRICS_TASK_MANAGER_RECORD_INTERVAL:
logger.debug(f"recording {self.prefix} metrics, last recorded {time_last_recorded} seconds ago")
self.subsystem_metrics.set(f"{self.prefix}_recorded_timestamp", current_time)
self.subsystem_metrics.pipe_execute()
else:
logger.debug('No nodes to spawn for %s', workflow_job.log_format)
for spawn_node in spawn_nodes:
if spawn_node.unified_job_template is None:
continue
kv = spawn_node.get_job_kwargs()
job = spawn_node.unified_job_template.create_unified_job(**kv)
spawn_node.job = job
spawn_node.save()
logger.debug('Spawned %s in %s for node %s', job.log_format, workflow_job.log_format, spawn_node.pk)
can_start = True
if isinstance(spawn_node.unified_job_template, WorkflowJobTemplate):
workflow_ancestors = job.get_ancestor_workflows()
if spawn_node.unified_job_template in set(workflow_ancestors):
can_start = False
logger.info(
'Refusing to start recursive workflow-in-workflow id={}, wfjt={}, ancestors={}'.format(
job.id, spawn_node.unified_job_template.pk, [wa.pk for wa in workflow_ancestors]
)
)
display_list = [spawn_node.unified_job_template] + workflow_ancestors
job.job_explanation = gettext_noop(
"Workflow Job spawned from workflow could not start because it " "would result in recursion (spawn order, most recent first: {})"
).format(', '.join(['<{}>'.format(tmp) for tmp in display_list]))
else:
logger.debug(
'Starting workflow-in-workflow id={}, wfjt={}, ancestors={}'.format(
job.id, spawn_node.unified_job_template.pk, [wa.pk for wa in workflow_ancestors]
)
)
if not job._resources_sufficient_for_launch():
can_start = False
job.job_explanation = gettext_noop(
"Job spawned from workflow could not start because it " "was missing a related resource such as project or inventory"
)
if can_start:
if workflow_job.start_args:
start_args = json.loads(decrypt_field(workflow_job, 'start_args'))
else:
start_args = {}
can_start = job.signal_start(**start_args)
if not can_start:
job.job_explanation = gettext_noop(
"Job spawned from workflow could not start because it " "was not in the right state or required manual credentials"
)
if not can_start:
job.status = 'failed'
job.save(update_fields=['status', 'job_explanation'])
job.websocket_emit_status('failed')
logger.debug(f"skipping recording {self.prefix} metrics, last recorded {time_last_recorded} seconds ago")
# TODO: should we emit a status on the socket here similar to tasks.py awx_periodic_scheduler() ?
# emit_websocket_notification('/socket.io/jobs', '', dict(id=))
def record_aggregate_metrics_and_exit(self, *args):
self.record_aggregate_metrics()
sys.exit(1)
def process_finished_workflow_jobs(self, workflow_jobs):
def schedule(self):
# Lock
with task_manager_bulk_reschedule():
with advisory_lock(f"{self.prefix}_lock", wait=False) as acquired:
with transaction.atomic():
if acquired is False:
logger.debug(f"Not running {self.prefix} scheduler, another task holds lock")
return
logger.debug(f"Starting {self.prefix} Scheduler")
# if sigterm due to timeout, still record metrics
signal.signal(signal.SIGTERM, self.record_aggregate_metrics_and_exit)
self._schedule()
commit_start = time.time()
if self.prefix == "task_manager":
self.subsystem_metrics.set(f"{self.prefix}_commit_seconds", time.time() - commit_start)
self.record_aggregate_metrics()
logger.debug(f"Finishing {self.prefix} Scheduler")
class WorkflowManager(TaskBase):
def __init__(self):
super().__init__(prefix="workflow_manager")
@timeit
def spawn_workflow_graph_jobs(self):
result = []
for workflow_job in workflow_jobs:
for workflow_job in self.all_tasks:
if self.timed_out():
logger.warning("Workflow manager has reached time out while processing running workflows, exiting loop early")
ScheduleWorkflowManager().schedule()
# Do not process any more workflow jobs. Stop here.
# Maybe we should schedule another WorkflowManager run
break
dag = WorkflowDAG(workflow_job)
status_changed = False
if workflow_job.cancel_flag:
@@ -190,99 +166,111 @@ class TaskManager:
status_changed = True
else:
workflow_nodes = dag.mark_dnr_nodes()
for n in workflow_nodes:
n.save(update_fields=['do_not_run'])
WorkflowJobNode.objects.bulk_update(workflow_nodes, ['do_not_run'])
# If workflow is now done, we do special things to mark it as done.
is_done = dag.is_workflow_done()
if not is_done:
continue
has_failed, reason = dag.has_workflow_failed()
logger.debug('Marking %s as %s.', workflow_job.log_format, 'failed' if has_failed else 'successful')
result.append(workflow_job.id)
new_status = 'failed' if has_failed else 'successful'
logger.debug("Transitioning {} to {} status.".format(workflow_job.log_format, new_status))
update_fields = ['status', 'start_args']
workflow_job.status = new_status
if reason:
logger.info(f'Workflow job {workflow_job.id} failed due to reason: {reason}')
workflow_job.job_explanation = gettext_noop("No error handling paths found, marking workflow as failed")
update_fields.append('job_explanation')
workflow_job.start_args = '' # blank field to remove encrypted passwords
workflow_job.save(update_fields=update_fields)
status_changed = True
if is_done:
has_failed, reason = dag.has_workflow_failed()
logger.debug('Marking %s as %s.', workflow_job.log_format, 'failed' if has_failed else 'successful')
result.append(workflow_job.id)
new_status = 'failed' if has_failed else 'successful'
logger.debug("Transitioning {} to {} status.".format(workflow_job.log_format, new_status))
update_fields = ['status', 'start_args']
workflow_job.status = new_status
if reason:
logger.info(f'Workflow job {workflow_job.id} failed due to reason: {reason}')
workflow_job.job_explanation = gettext_noop("No error handling paths found, marking workflow as failed")
update_fields.append('job_explanation')
workflow_job.start_args = '' # blank field to remove encrypted passwords
workflow_job.save(update_fields=update_fields)
status_changed = True
if status_changed:
if workflow_job.spawned_by_workflow:
ScheduleWorkflowManager().schedule()
workflow_job.websocket_emit_status(workflow_job.status)
# Operations whose queries rely on modifications made during the atomic scheduling session
workflow_job.send_notification_templates('succeeded' if workflow_job.status == 'successful' else 'failed')
if workflow_job.spawned_by_workflow:
schedule_task_manager()
if workflow_job.status == 'running':
spawn_nodes = dag.bfs_nodes_to_run()
if spawn_nodes:
logger.debug('Spawning jobs for %s', workflow_job.log_format)
else:
logger.debug('No nodes to spawn for %s', workflow_job.log_format)
for spawn_node in spawn_nodes:
if spawn_node.unified_job_template is None:
continue
kv = spawn_node.get_job_kwargs()
job = spawn_node.unified_job_template.create_unified_job(**kv)
spawn_node.job = job
spawn_node.save()
logger.debug('Spawned %s in %s for node %s', job.log_format, workflow_job.log_format, spawn_node.pk)
can_start = True
if isinstance(spawn_node.unified_job_template, WorkflowJobTemplate):
workflow_ancestors = job.get_ancestor_workflows()
if spawn_node.unified_job_template in set(workflow_ancestors):
can_start = False
logger.info(
'Refusing to start recursive workflow-in-workflow id={}, wfjt={}, ancestors={}'.format(
job.id, spawn_node.unified_job_template.pk, [wa.pk for wa in workflow_ancestors]
)
)
display_list = [spawn_node.unified_job_template] + workflow_ancestors
job.job_explanation = gettext_noop(
"Workflow Job spawned from workflow could not start because it "
"would result in recursion (spawn order, most recent first: {})"
).format(', '.join('<{}>'.format(tmp) for tmp in display_list))
else:
logger.debug(
'Starting workflow-in-workflow id={}, wfjt={}, ancestors={}'.format(
job.id, spawn_node.unified_job_template.pk, [wa.pk for wa in workflow_ancestors]
)
)
if not job._resources_sufficient_for_launch():
can_start = False
job.job_explanation = gettext_noop(
"Job spawned from workflow could not start because it was missing a related resource such as project or inventory"
)
if can_start:
if workflow_job.start_args:
start_args = json.loads(decrypt_field(workflow_job, 'start_args'))
else:
start_args = {}
can_start = job.signal_start(**start_args)
if not can_start:
job.job_explanation = gettext_noop(
"Job spawned from workflow could not start because it was not in the right state or required manual credentials"
)
if not can_start:
job.status = 'failed'
job.save(update_fields=['status', 'job_explanation'])
job.websocket_emit_status('failed')
# TODO: should we emit a status on the socket here similar to tasks.py awx_periodic_scheduler() ?
# emit_websocket_notification('/socket.io/jobs', '', dict(id=))
return result
def start_task(self, task, instance_group, dependent_tasks=None, instance=None):
self.start_task_limit -= 1
if self.start_task_limit == 0:
# schedule another run immediately after this task manager
schedule_task_manager()
from awx.main.tasks.system import handle_work_error, handle_work_success
@timeit
def get_tasks(self, filter_args):
self.all_tasks = [wf for wf in WorkflowJob.objects.filter(**filter_args)]
dependent_tasks = dependent_tasks or []
@timeit
def _schedule(self):
self.get_tasks(dict(status__in=["running"], dependencies_processed=True))
if len(self.all_tasks) > 0:
self.spawn_workflow_graph_jobs()
task_actual = {
'type': get_type_for_model(type(task)),
'id': task.id,
}
dependencies = [{'type': get_type_for_model(type(t)), 'id': t.id} for t in dependent_tasks]
task.status = 'waiting'
class DependencyManager(TaskBase):
def __init__(self):
super().__init__(prefix="dependency_manager")
(start_status, opts) = task.pre_start()
if not start_status:
task.status = 'failed'
if task.job_explanation:
task.job_explanation += ' '
task.job_explanation += 'Task failed pre-start check.'
task.save()
# TODO: run error handler to fail sub-tasks and send notifications
else:
if type(task) is WorkflowJob:
task.status = 'running'
task.send_notification_templates('running')
logger.debug('Transitioning %s to running status.', task.log_format)
schedule_task_manager()
# at this point we already have control/execution nodes selected for the following cases
else:
task.instance_group = instance_group
execution_node_msg = f' and execution node {task.execution_node}' if task.execution_node else ''
logger.debug(
f'Submitting job {task.log_format} controlled by {task.controller_node} to instance group {instance_group.name}{execution_node_msg}.'
)
with disable_activity_stream():
task.celery_task_id = str(uuid.uuid4())
task.save()
task.log_lifecycle("waiting")
def post_commit():
if task.status != 'failed' and type(task) is not WorkflowJob:
# Before task is dispatched, ensure that job_event partitions exist
create_partition(task.event_class._meta.db_table, start=task.created)
task_cls = task._get_task_class()
task_cls.apply_async(
[task.pk],
opts,
queue=task.get_queue_name(),
uuid=task.celery_task_id,
callbacks=[{'task': handle_work_success.name, 'kwargs': {'task_actual': task_actual}}],
errbacks=[{'task': handle_work_error.name, 'args': [task.celery_task_id], 'kwargs': {'subtasks': [task_actual] + dependencies}}],
)
task.websocket_emit_status(task.status) # adds to on_commit
connection.on_commit(post_commit)
def process_running_tasks(self, running_tasks):
for task in running_tasks:
self.dependency_graph.add_job(task)
def create_project_update(self, task):
project_task = Project.objects.get(id=task.project_id).create_project_update(_eager_fields=dict(launch_type='dependency'))
def create_project_update(self, task, project_id=None):
if project_id is None:
project_id = task.project_id
project_task = Project.objects.get(id=project_id).create_project_update(_eager_fields=dict(launch_type='dependency'))
# Project created 1 seconds behind
project_task.created = task.created - timedelta(seconds=1)
@@ -298,17 +286,19 @@ class TaskManager:
inventory_task.status = 'pending'
inventory_task.save()
logger.debug('Spawned {} as dependency of {}'.format(inventory_task.log_format, task.log_format))
# inventory_sources = self.get_inventory_source_tasks([task])
# self.process_inventory_sources(inventory_sources)
return inventory_task
def capture_chain_failure_dependencies(self, task, dependencies):
def add_dependencies(self, task, dependencies):
with disable_activity_stream():
task.dependent_jobs.add(*dependencies)
for dep in dependencies:
# Add task + all deps except self
dep.dependent_jobs.add(*([task] + [d for d in dependencies if d != dep]))
def get_inventory_source_tasks(self):
inventory_ids = set()
for task in self.all_tasks:
if isinstance(task, Job):
inventory_ids.add(task.inventory_id)
self.all_inventory_sources = [invsrc for invsrc in InventorySource.objects.filter(inventory_id__in=inventory_ids, update_on_launch=True)]
def get_latest_inventory_update(self, inventory_source):
latest_inventory_update = InventoryUpdate.objects.filter(inventory_source=inventory_source).order_by("-created")
@@ -335,8 +325,8 @@ class TaskManager:
return True
return False
def get_latest_project_update(self, job):
latest_project_update = ProjectUpdate.objects.filter(project=job.project, job_type='check').order_by("-created")
def get_latest_project_update(self, project_id):
latest_project_update = ProjectUpdate.objects.filter(project=project_id, job_type='check').order_by("-created")
if not latest_project_update.exists():
return None
return latest_project_update.first()
@@ -376,55 +366,233 @@ class TaskManager:
return True
return False
def gen_dep_for_job(self, task):
created_dependencies = []
dependencies = []
# TODO: Can remove task.project None check after scan-job-default-playbook is removed
if task.project is not None and task.project.scm_update_on_launch is True:
latest_project_update = self.get_latest_project_update(task.project_id)
if self.should_update_related_project(task, latest_project_update):
latest_project_update = self.create_project_update(task)
created_dependencies.append(latest_project_update)
dependencies.append(latest_project_update)
# Inventory created 2 seconds behind job
try:
start_args = json.loads(decrypt_field(task, field_name="start_args"))
except ValueError:
start_args = dict()
# generator for inventory sources related to this task
task_inv_sources = (invsrc for invsrc in self.all_inventory_sources if invsrc.inventory_id == task.inventory_id)
for inventory_source in task_inv_sources:
if "inventory_sources_already_updated" in start_args and inventory_source.id in start_args['inventory_sources_already_updated']:
continue
if not inventory_source.update_on_launch:
continue
latest_inventory_update = self.get_latest_inventory_update(inventory_source)
if self.should_update_inventory_source(task, latest_inventory_update):
inventory_task = self.create_inventory_update(task, inventory_source)
created_dependencies.append(inventory_task)
dependencies.append(inventory_task)
else:
dependencies.append(latest_inventory_update)
if dependencies:
self.add_dependencies(task, dependencies)
return created_dependencies
def gen_dep_for_inventory_update(self, inventory_task):
created_dependencies = []
if inventory_task.source == "scm":
invsrc = inventory_task.inventory_source
if not invsrc.source_project.scm_update_on_launch:
return created_dependencies
latest_src_project_update = self.get_latest_project_update(invsrc.source_project_id)
if self.should_update_related_project(inventory_task, latest_src_project_update):
latest_src_project_update = self.create_project_update(inventory_task, project_id=invsrc.source_project_id)
created_dependencies.append(latest_src_project_update)
self.add_dependencies(inventory_task, [latest_src_project_update])
latest_src_project_update.scm_inventory_updates.add(inventory_task)
return created_dependencies
@timeit
def generate_dependencies(self, undeped_tasks):
created_dependencies = []
for task in undeped_tasks:
task.log_lifecycle("acknowledged")
dependencies = []
if not type(task) is Job:
if type(task) is Job:
created_dependencies += self.gen_dep_for_job(task)
elif type(task) is InventoryUpdate:
created_dependencies += self.gen_dep_for_inventory_update(task)
else:
continue
# TODO: Can remove task.project None check after scan-job-default-playbook is removed
if task.project is not None and task.project.scm_update_on_launch is True:
latest_project_update = self.get_latest_project_update(task)
if self.should_update_related_project(task, latest_project_update):
project_task = self.create_project_update(task)
created_dependencies.append(project_task)
dependencies.append(project_task)
else:
dependencies.append(latest_project_update)
# Inventory created 2 seconds behind job
try:
start_args = json.loads(decrypt_field(task, field_name="start_args"))
except ValueError:
start_args = dict()
for inventory_source in [invsrc for invsrc in self.all_inventory_sources if invsrc.inventory == task.inventory]:
if "inventory_sources_already_updated" in start_args and inventory_source.id in start_args['inventory_sources_already_updated']:
continue
if not inventory_source.update_on_launch:
continue
latest_inventory_update = self.get_latest_inventory_update(inventory_source)
if self.should_update_inventory_source(task, latest_inventory_update):
inventory_task = self.create_inventory_update(task, inventory_source)
created_dependencies.append(inventory_task)
dependencies.append(inventory_task)
else:
dependencies.append(latest_inventory_update)
if len(dependencies) > 0:
self.capture_chain_failure_dependencies(task, dependencies)
UnifiedJob.objects.filter(pk__in=[task.pk for task in undeped_tasks]).update(dependencies_processed=True)
return created_dependencies
def process_tasks(self):
deps = self.generate_dependencies(self.all_tasks)
self.generate_dependencies(deps)
self.subsystem_metrics.inc(f"{self.prefix}_pending_processed", len(self.all_tasks) + len(deps))
@timeit
def _schedule(self):
self.get_tasks(dict(status__in=["pending"], dependencies_processed=False))
if len(self.all_tasks) > 0:
self.get_inventory_source_tasks()
self.process_tasks()
ScheduleTaskManager().schedule()
class TaskManager(TaskBase):
def __init__(self):
"""
Do NOT put database queries or other potentially expensive operations
in the task manager init. The task manager object is created every time a
job is created, transitions state, and every 30 seconds on each tower node.
More often then not, the object is destroyed quickly because the NOOP case is hit.
The NOOP case is short-circuit logic. If the task manager realizes that another instance
of the task manager is already running, then it short-circuits and decides not to run.
"""
# start task limit indicates how many pending jobs can be started on this
# .schedule() run. Starting jobs is expensive, and there is code in place to reap
# the task manager after 5 minutes. At scale, the task manager can easily take more than
# 5 minutes to start pending jobs. If this limit is reached, pending jobs
# will no longer be started and will be started on the next task manager cycle.
self.time_delta_job_explanation = timedelta(seconds=30)
super().__init__(prefix="task_manager")
def after_lock_init(self):
"""
Init AFTER we know this instance of the task manager will run because the lock is acquired.
"""
self.dependency_graph = DependencyGraph()
self.instances = TaskManagerInstances(self.all_tasks)
self.instance_groups = TaskManagerInstanceGroups(instances_by_hostname=self.instances)
self.controlplane_ig = self.instance_groups.controlplane_ig
def job_blocked_by(self, task):
# TODO: I'm not happy with this, I think blocking behavior should be decided outside of the dependency graph
# in the old task manager this was handled as a method on each task object outside of the graph and
# probably has the side effect of cutting down *a lot* of the logic from this task manager class
blocked_by = self.dependency_graph.task_blocked_by(task)
if blocked_by:
return blocked_by
for dep in task.dependent_jobs.all():
if dep.status in ACTIVE_STATES:
return dep
# if we detect a failed or error dependency, go ahead and fail this
# task. The errback on the dependency takes some time to trigger,
# and we don't want the task to enter running state if its
# dependency has failed or errored.
elif dep.status in ("error", "failed"):
task.status = 'failed'
task.job_explanation = 'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % (
get_type_for_model(type(dep)),
dep.name,
dep.id,
)
task.save(update_fields=['status', 'job_explanation'])
task.websocket_emit_status('failed')
return dep
return None
@timeit
def start_task(self, task, instance_group, dependent_tasks=None, instance=None):
self.dependency_graph.add_job(task)
self.subsystem_metrics.inc(f"{self.prefix}_tasks_started", 1)
self.start_task_limit -= 1
if self.start_task_limit == 0:
# schedule another run immediately after this task manager
ScheduleTaskManager().schedule()
from awx.main.tasks.system import handle_work_error, handle_work_success
# update capacity for control node and execution node
if task.controller_node:
self.instances[task.controller_node].consume_capacity(settings.AWX_CONTROL_NODE_TASK_IMPACT)
if task.execution_node:
self.instances[task.execution_node].consume_capacity(task.task_impact)
dependent_tasks = dependent_tasks or []
task_actual = {
'type': get_type_for_model(type(task)),
'id': task.id,
}
dependencies = [{'type': get_type_for_model(type(t)), 'id': t.id} for t in dependent_tasks]
task.status = 'waiting'
(start_status, opts) = task.pre_start()
if not start_status:
task.status = 'failed'
if task.job_explanation:
task.job_explanation += ' '
task.job_explanation += 'Task failed pre-start check.'
task.save()
# TODO: run error handler to fail sub-tasks and send notifications
else:
if type(task) is WorkflowJob:
task.status = 'running'
task.send_notification_templates('running')
logger.debug('Transitioning %s to running status.', task.log_format)
# Call this to ensure Workflow nodes get spawned in timely manner
ScheduleWorkflowManager().schedule()
# at this point we already have control/execution nodes selected for the following cases
else:
task.instance_group = instance_group
execution_node_msg = f' and execution node {task.execution_node}' if task.execution_node else ''
logger.debug(
f'Submitting job {task.log_format} controlled by {task.controller_node} to instance group {instance_group.name}{execution_node_msg}.'
)
with disable_activity_stream():
task.celery_task_id = str(uuid.uuid4())
task.save()
task.log_lifecycle("waiting")
# apply_async does a NOTIFY to the channel dispatcher is listening to
# postgres will treat this as part of the transaction, which is what we want
if task.status != 'failed' and type(task) is not WorkflowJob:
task_cls = task._get_task_class()
task_cls.apply_async(
[task.pk],
opts,
queue=task.get_queue_name(),
uuid=task.celery_task_id,
callbacks=[{'task': handle_work_success.name, 'kwargs': {'task_actual': task_actual}}],
errbacks=[{'task': handle_work_error.name, 'args': [task.celery_task_id], 'kwargs': {'subtasks': [task_actual] + dependencies}}],
)
# In exception cases, like a job failing pre-start checks, we send the websocket status message
# for jobs going into waiting, we omit this because of performance issues, as it should go to running quickly
if task.status != 'waiting':
task.websocket_emit_status(task.status) # adds to on_commit
@timeit
def process_running_tasks(self, running_tasks):
for task in running_tasks:
if type(task) is WorkflowJob:
ScheduleWorkflowManager().schedule()
self.dependency_graph.add_job(task)
@timeit
def process_pending_tasks(self, pending_tasks):
running_workflow_templates = {wf.unified_job_template_id for wf in self.get_running_workflow_jobs()}
tasks_to_update_job_explanation = []
for task in pending_tasks:
if self.start_task_limit <= 0:
break
if self.timed_out():
logger.warning("Task manager has reached time out while processing pending jobs, exiting loop early")
break
blocked_by = self.job_blocked_by(task)
if blocked_by:
self.subsystem_metrics.inc(f"{self.prefix}_tasks_blocked", 1)
task.log_lifecycle("blocked", blocked_by=blocked_by)
job_explanation = gettext_noop(f"waiting for {blocked_by._meta.model_name}-{blocked_by.id} to finish")
if task.job_explanation != job_explanation:
@@ -433,19 +601,16 @@ class TaskManager:
tasks_to_update_job_explanation.append(task)
continue
found_acceptable_queue = False
preferred_instance_groups = task.preferred_instance_groups
if isinstance(task, WorkflowJob):
if task.unified_job_template_id in running_workflow_templates:
if not task.allow_simultaneous:
logger.debug("{} is blocked from running, workflow already running".format(task.log_format))
continue
else:
running_workflow_templates.add(task.unified_job_template_id)
# Previously we were tracking allow_simultaneous blocking both here and in DependencyGraph.
# Double check that using just the DependencyGraph works for Workflows and Sliced Jobs.
self.start_task(task, None, task.get_jobs_fail_chain(), None)
continue
found_acceptable_queue = False
preferred_instance_groups = self.instance_groups.get_instance_groups_from_task_cache(task)
# Determine if there is control capacity for the task
if task.capacity_type == 'control':
control_impact = task.task_impact + settings.AWX_CONTROL_NODE_TASK_IMPACT
@@ -464,8 +629,6 @@ class TaskManager:
# All task.capacity_type == 'control' jobs should run on control plane, no need to loop over instance groups
if task.capacity_type == 'control':
task.execution_node = control_instance.hostname
control_instance.consume_capacity(control_impact)
self.dependency_graph.add_job(task)
execution_instance = self.instances[control_instance.hostname].obj
task.log_lifecycle("controller_node_chosen")
task.log_lifecycle("execution_node_chosen")
@@ -475,7 +638,6 @@ class TaskManager:
for instance_group in preferred_instance_groups:
if instance_group.is_container_group:
self.dependency_graph.add_job(task)
self.start_task(task, instance_group, task.get_jobs_fail_chain(), None)
found_acceptable_queue = True
break
@@ -497,9 +659,7 @@ class TaskManager:
control_instance = execution_instance
task.controller_node = execution_instance.hostname
control_instance.consume_capacity(settings.AWX_CONTROL_NODE_TASK_IMPACT)
task.log_lifecycle("controller_node_chosen")
execution_instance.consume_capacity(task.task_impact)
task.log_lifecycle("execution_node_chosen")
logger.debug(
"Starting {} in group {} instance {} (remaining_capacity={})".format(
@@ -507,7 +667,6 @@ class TaskManager:
)
)
execution_instance = self.instances[execution_instance.hostname].obj
self.dependency_graph.add_job(task)
self.start_task(task, instance_group, task.get_jobs_fail_chain(), execution_instance)
found_acceptable_queue = True
break
@@ -533,25 +692,6 @@ class TaskManager:
tasks_to_update_job_explanation.append(task)
logger.debug("{} couldn't be scheduled on graph, waiting for next cycle".format(task.log_format))
def timeout_approval_node(self):
workflow_approvals = WorkflowApproval.objects.filter(status='pending')
now = tz_now()
for task in workflow_approvals:
approval_timeout_seconds = timedelta(seconds=task.timeout)
if task.timeout == 0:
continue
if (now - task.created) >= approval_timeout_seconds:
timeout_message = _("The approval node {name} ({pk}) has expired after {timeout} seconds.").format(
name=task.name, pk=task.pk, timeout=task.timeout
)
logger.warning(timeout_message)
task.timed_out = True
task.status = 'failed'
task.send_approval_notification('timed_out')
task.websocket_emit_status(task.status)
task.job_explanation = timeout_message
task.save(update_fields=['status', 'job_explanation', 'timed_out'])
def reap_jobs_from_orphaned_instances(self):
# discover jobs that are in running state but aren't on an execution node
# that we know about; this is a fairly rare event, but it can occur if you,
@@ -564,60 +704,45 @@ class TaskManager:
logger.error(f'{j.execution_node} is not a registered instance; reaping {j.log_format}')
reap_job(j, 'failed')
def process_tasks(self, all_sorted_tasks):
running_tasks = [t for t in all_sorted_tasks if t.status in ['waiting', 'running']]
def process_tasks(self):
running_tasks = [t for t in self.all_tasks if t.status in ['waiting', 'running']]
self.process_running_tasks(running_tasks)
self.subsystem_metrics.inc(f"{self.prefix}_running_processed", len(running_tasks))
pending_tasks = [t for t in self.all_tasks if t.status == 'pending']
pending_tasks = [t for t in all_sorted_tasks if t.status == 'pending']
undeped_tasks = [t for t in pending_tasks if not t.dependencies_processed]
dependencies = self.generate_dependencies(undeped_tasks)
self.process_pending_tasks(dependencies)
self.process_pending_tasks(pending_tasks)
self.subsystem_metrics.inc(f"{self.prefix}_pending_processed", len(pending_tasks))
def timeout_approval_node(self, task):
if self.timed_out():
logger.warning("Task manager has reached time out while processing approval nodes, exiting loop early")
# Do not process any more workflow approval nodes. Stop here.
# Maybe we should schedule another TaskManager run
return
timeout_message = _("The approval node {name} ({pk}) has expired after {timeout} seconds.").format(name=task.name, pk=task.pk, timeout=task.timeout)
logger.warning(timeout_message)
task.timed_out = True
task.status = 'failed'
task.send_approval_notification('timed_out')
task.websocket_emit_status(task.status)
task.job_explanation = timeout_message
task.save(update_fields=['status', 'job_explanation', 'timed_out'])
def get_expired_workflow_approvals(self):
# timeout of 0 indicates that it never expires
qs = WorkflowApproval.objects.filter(status='pending').exclude(timeout=0).filter(expires__lt=tz_now())
return qs
@timeit
def _schedule(self):
finished_wfjs = []
all_sorted_tasks = self.get_tasks()
self.get_tasks(dict(status__in=["pending", "waiting", "running"], dependencies_processed=True))
self.after_lock_init(all_sorted_tasks)
self.after_lock_init()
self.reap_jobs_from_orphaned_instances()
if len(all_sorted_tasks) > 0:
# TODO: Deal with
# latest_project_updates = self.get_latest_project_update_tasks(all_sorted_tasks)
# self.process_latest_project_updates(latest_project_updates)
if len(self.all_tasks) > 0:
self.process_tasks()
# latest_inventory_updates = self.get_latest_inventory_update_tasks(all_sorted_tasks)
# self.process_latest_inventory_updates(latest_inventory_updates)
self.all_inventory_sources = self.get_inventory_source_tasks(all_sorted_tasks)
running_workflow_tasks = self.get_running_workflow_jobs()
finished_wfjs = self.process_finished_workflow_jobs(running_workflow_tasks)
previously_running_workflow_tasks = running_workflow_tasks
running_workflow_tasks = []
for workflow_job in previously_running_workflow_tasks:
if workflow_job.status == 'running':
running_workflow_tasks.append(workflow_job)
else:
logger.debug('Removed %s from job spawning consideration.', workflow_job.log_format)
self.spawn_workflow_graph_jobs(running_workflow_tasks)
self.timeout_approval_node()
self.reap_jobs_from_orphaned_instances()
self.process_tasks(all_sorted_tasks)
return finished_wfjs
def schedule(self):
# Lock
with advisory_lock('task_manager_lock', wait=False) as acquired:
with transaction.atomic():
if acquired is False:
logger.debug("Not running scheduler, another task holds lock")
return
logger.debug("Starting Scheduler")
with task_manager_bulk_reschedule():
self._schedule()
logger.debug("Finishing Scheduler")
for workflow_approval in self.get_expired_workflow_approvals():
self.timeout_approval_node(workflow_approval)

View File

@@ -34,12 +34,10 @@ class TaskManagerInstance:
class TaskManagerInstances:
def __init__(self, active_tasks, instances=None):
def __init__(self, active_tasks, instances=None, instance_fields=('node_type', 'capacity', 'hostname', 'enabled')):
self.instances_by_hostname = dict()
if instances is None:
instances = (
Instance.objects.filter(hostname__isnull=False, enabled=True).exclude(node_type='hop').only('node_type', 'capacity', 'hostname', 'enabled')
)
instances = Instance.objects.filter(hostname__isnull=False, enabled=True).exclude(node_type='hop').only(*instance_fields)
for instance in instances:
self.instances_by_hostname[instance.hostname] = TaskManagerInstance(instance)
@@ -67,6 +65,7 @@ class TaskManagerInstanceGroups:
def __init__(self, instances_by_hostname=None, instance_groups=None, instance_groups_queryset=None):
self.instance_groups = dict()
self.controlplane_ig = None
self.pk_ig_map = dict()
if instance_groups is not None: # for testing
self.instance_groups = instance_groups
@@ -81,6 +80,7 @@ class TaskManagerInstanceGroups:
instances_by_hostname[instance.hostname] for instance in instance_group.instances.all() if instance.hostname in instances_by_hostname
],
)
self.pk_ig_map[instance_group.pk] = instance_group
def get_remaining_capacity(self, group_name):
instances = self.instance_groups[group_name]['instances']
@@ -121,3 +121,17 @@ class TaskManagerInstanceGroups:
elif i.capacity > largest_instance.capacity:
largest_instance = i
return largest_instance
def get_instance_groups_from_task_cache(self, task):
igs = []
if task.preferred_instance_groups_cache:
for pk in task.preferred_instance_groups_cache:
ig = self.pk_ig_map.get(pk, None)
if ig:
igs.append(ig)
else:
logger.warn(f"Unknown instance group with pk {pk} for task {task}")
if len(igs) == 0:
logger.warn(f"No instance groups in cache exist, defaulting to global instance groups for task {task}")
return task.global_instance_groups
return igs

View File

@@ -1,15 +1,35 @@
# Python
import logging
# Django
from django.conf import settings
# AWX
from awx.main.scheduler import TaskManager
from awx import MODE
from awx.main.scheduler import TaskManager, DependencyManager, WorkflowManager
from awx.main.dispatch.publish import task
from awx.main.dispatch import get_local_queuename
logger = logging.getLogger('awx.main.scheduler')
def run_manager(manager, prefix):
if MODE == 'development' and settings.AWX_DISABLE_TASK_MANAGERS:
logger.debug(f"Not running {prefix} manager, AWX_DISABLE_TASK_MANAGERS is True. Trigger with GET to /api/debug/{prefix}_manager/")
return
manager().schedule()
@task(queue=get_local_queuename)
def run_task_manager():
logger.debug("Running task manager.")
TaskManager().schedule()
def task_manager():
run_manager(TaskManager, "task")
@task(queue=get_local_queuename)
def dependency_manager():
run_manager(DependencyManager, "dependency")
@task(queue=get_local_queuename)
def workflow_manager():
run_manager(WorkflowManager, "workflow")

View File

@@ -409,7 +409,7 @@ def emit_activity_stream_change(instance):
from awx.api.serializers import ActivityStreamSerializer
actor = None
if instance.actor:
if instance.actor_id:
actor = instance.actor.username
summary_fields = ActivityStreamSerializer(instance).get_summary_fields(instance)
analytics_logger.info(

View File

@@ -6,13 +6,14 @@ import os
import stat
# Django
from django.utils.timezone import now
from django.conf import settings
from django_guid import get_guid
from django.utils.functional import cached_property
from django.db import connections
# AWX
from awx.main.redact import UriCleaner
from awx.main.constants import MINIMAL_EVENTS
from awx.main.constants import MINIMAL_EVENTS, ANSIBLE_RUNNER_NEEDS_UPDATE_MESSAGE
from awx.main.utils.update_model import update_model
from awx.main.queue import CallbackQueueDispatcher
@@ -20,8 +21,6 @@ logger = logging.getLogger('awx.main.tasks.callback')
class RunnerCallback:
event_data_key = 'job_id'
def __init__(self, model=None):
self.parent_workflow_job_id = None
self.host_map = {}
@@ -33,10 +32,40 @@ class RunnerCallback:
self.event_ct = 0
self.model = model
self.update_attempts = int(settings.DISPATCHER_DB_DOWNTOWN_TOLLERANCE / 5)
self.wrapup_event_dispatched = False
self.extra_update_fields = {}
def update_model(self, pk, _attempt=0, **updates):
return update_model(self.model, pk, _attempt=0, _max_attempts=self.update_attempts, **updates)
@cached_property
def wrapup_event_type(self):
return self.instance.event_class.WRAPUP_EVENT
@cached_property
def event_data_key(self):
return self.instance.event_class.JOB_REFERENCE
def delay_update(self, skip_if_already_set=False, **kwargs):
"""Stash fields that should be saved along with the job status change"""
for key, value in kwargs.items():
if key in self.extra_update_fields and skip_if_already_set:
continue
elif key in self.extra_update_fields and key in ('job_explanation', 'result_traceback'):
if str(value) in self.extra_update_fields.get(key, ''):
continue # if already set, avoid duplicating messages
# In the case of these fields, we do not want to lose any prior information, so combine values
self.extra_update_fields[key] = '\n'.join([str(self.extra_update_fields[key]), str(value)])
else:
self.extra_update_fields[key] = value
def get_delayed_update_fields(self):
"""Return finalized dict of all fields that should be saved along with the job status change"""
self.extra_update_fields['emitted_events'] = self.event_ct
if 'got an unexpected keyword argument' in self.extra_update_fields.get('result_traceback', ''):
self.delay_update(result_traceback=ANSIBLE_RUNNER_NEEDS_UPDATE_MESSAGE)
return self.extra_update_fields
def event_handler(self, event_data):
#
# ⚠️ D-D-D-DANGER ZONE ⚠️
@@ -130,6 +159,9 @@ class RunnerCallback:
elif self.recent_event_timings.maxlen:
self.recent_event_timings.append(time.time())
if event_data.get('event', '') == self.wrapup_event_type:
self.wrapup_event_dispatched = True
event_data.setdefault(self.event_data_key, self.instance.id)
self.dispatcher.dispatch(event_data)
self.event_ct += 1
@@ -138,27 +170,10 @@ class RunnerCallback:
Handle artifacts
'''
if event_data.get('event_data', {}).get('artifact_data', {}):
self.instance.artifacts = event_data['event_data']['artifact_data']
self.instance.save(update_fields=['artifacts'])
self.delay_update(artifacts=event_data['event_data']['artifact_data'])
return False
def cancel_callback(self):
"""
Ansible runner callback to tell the job when/if it is canceled
"""
unified_job_id = self.instance.pk
self.instance = self.update_model(unified_job_id)
if not self.instance:
logger.error('unified job {} was deleted while running, canceling'.format(unified_job_id))
return True
if self.instance.cancel_flag or self.instance.status == 'canceled':
cancel_wait = (now() - self.instance.modified).seconds if self.instance.modified else 0
if cancel_wait > 5:
logger.warning('Request to cancel {} took {} seconds to complete.'.format(self.instance.log_format, cancel_wait))
return True
return False
def finished_callback(self, runner_obj):
"""
Ansible runner callback triggered on finished run
@@ -170,6 +185,8 @@ class RunnerCallback:
}
event_data.setdefault(self.event_data_key, self.instance.id)
self.dispatcher.dispatch(event_data)
if self.wrapup_event_type == 'EOF':
self.wrapup_event_dispatched = True
def status_handler(self, status_data, runner_config):
"""
@@ -187,6 +204,8 @@ class RunnerCallback:
with disable_activity_stream():
self.instance = self.update_model(self.instance.pk, job_args=json.dumps(runner_config.command), job_cwd=runner_config.cwd, job_env=job_env)
# We opened a connection just for that save, close it here now
connections.close_all()
elif status_data['status'] == 'failed':
# For encrypted ssh_key_data, ansible-runner worker will open and write the
# ssh_key_data to a named pipe. Then, once the podman container starts, ssh-agent will
@@ -205,16 +224,10 @@ class RunnerCallback:
elif status_data['status'] == 'error':
result_traceback = status_data.get('result_traceback', None)
if result_traceback:
from awx.main.signals import disable_activity_stream # Circular import
with disable_activity_stream():
self.instance = self.update_model(self.instance.pk, result_traceback=result_traceback)
self.delay_update(result_traceback=result_traceback)
class RunnerCallbackForProjectUpdate(RunnerCallback):
event_data_key = 'project_update_id'
def __init__(self, *args, **kwargs):
super(RunnerCallbackForProjectUpdate, self).__init__(*args, **kwargs)
self.playbook_new_revision = None
@@ -231,9 +244,6 @@ class RunnerCallbackForProjectUpdate(RunnerCallback):
class RunnerCallbackForInventoryUpdate(RunnerCallback):
event_data_key = 'inventory_update_id'
def __init__(self, *args, **kwargs):
super(RunnerCallbackForInventoryUpdate, self).__init__(*args, **kwargs)
self.end_line = 0
@@ -245,9 +255,6 @@ class RunnerCallbackForInventoryUpdate(RunnerCallback):
class RunnerCallbackForAdHocCommand(RunnerCallback):
event_data_key = 'ad_hoc_command_id'
def __init__(self, *args, **kwargs):
super(RunnerCallbackForAdHocCommand, self).__init__(*args, **kwargs)
self.host_map = {}
@@ -255,4 +262,4 @@ class RunnerCallbackForAdHocCommand(RunnerCallback):
class RunnerCallbackForSystemJob(RunnerCallback):
event_data_key = 'system_job_id'
pass

View File

@@ -1,6 +1,5 @@
# Python
from collections import OrderedDict
from distutils.dir_util import copy_tree
import errno
import functools
import fcntl
@@ -15,11 +14,9 @@ import tempfile
import traceback
import time
import urllib.parse as urlparse
from uuid import uuid4
# Django
from django.conf import settings
from django.db import transaction
# Runner
@@ -34,13 +31,12 @@ from gitdb.exc import BadName as BadGitName
from awx.main.dispatch.publish import task
from awx.main.dispatch import get_local_queuename
from awx.main.constants import (
ACTIVE_STATES,
PRIVILEGE_ESCALATION_METHODS,
STANDARD_INVENTORY_UPDATE_ENV,
JOB_FOLDER_PREFIX,
MAX_ISOLATED_PATH_COLON_DELIMITER,
CONTAINER_VOLUMES_MOUNT_TYPES,
ANSIBLE_RUNNER_NEEDS_UPDATE_MESSAGE,
ACTIVE_STATES,
)
from awx.main.models import (
Instance,
@@ -65,6 +61,7 @@ from awx.main.tasks.callback import (
RunnerCallbackForProjectUpdate,
RunnerCallbackForSystemJob,
)
from awx.main.tasks.signals import with_signal_handling, signal_callback
from awx.main.tasks.receptor import AWXReceptorJob
from awx.main.exceptions import AwxTaskError, PostRunError, ReceptorNodeNotFound
from awx.main.utils.ansible import read_ansible_config
@@ -78,7 +75,7 @@ from awx.main.utils.common import (
)
from awx.conf.license import get_license
from awx.main.utils.handlers import SpecialInventoryHandler
from awx.main.tasks.system import handle_success_and_failure_notifications, update_smart_memberships_for_inventory, update_inventory_computed_fields
from awx.main.tasks.system import update_smart_memberships_for_inventory, update_inventory_computed_fields
from awx.main.utils.update_model import update_model
from rest_framework.exceptions import PermissionDenied
from django.utils.translation import gettext_lazy as _
@@ -119,12 +116,11 @@ class BaseTask(object):
def update_model(self, pk, _attempt=0, **updates):
return update_model(self.model, pk, _attempt=0, _max_attempts=self.update_attempts, **updates)
def write_private_data_file(self, private_data_dir, file_name, data, sub_dir=None, permissions=0o600):
def write_private_data_file(self, private_data_dir, file_name, data, sub_dir=None, file_permissions=0o600):
base_path = private_data_dir
if sub_dir:
base_path = os.path.join(private_data_dir, sub_dir)
if not os.path.exists(base_path):
os.mkdir(base_path, 0o700)
os.makedirs(base_path, mode=0o700, exist_ok=True)
# If we got a file name create it, otherwise we want a temp file
if file_name:
@@ -134,7 +130,7 @@ class BaseTask(object):
os.close(handle)
file = Path(file_path)
file.touch(mode=permissions, exist_ok=True)
file.touch(mode=file_permissions, exist_ok=True)
with open(file_path, 'w') as f:
f.write(data)
return file_path
@@ -214,14 +210,22 @@ class BaseTask(object):
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
if settings.AWX_CLEANUP_PATHS:
self.cleanup_paths.append(path)
# Ansible runner requires that project exists,
# and we will write files in the other folders without pre-creating the folder
for subfolder in ('project', 'inventory', 'env'):
# We will write files in these folders later
for subfolder in ('inventory', 'env'):
runner_subfolder = os.path.join(path, subfolder)
if not os.path.exists(runner_subfolder):
os.mkdir(runner_subfolder)
return path
def build_project_dir(self, instance, private_data_dir):
"""
Create the ansible-runner project subdirectory. In many cases this is the source checkout.
In cases that do not even need the source checkout, we create an empty dir to be the workdir.
"""
project_dir = os.path.join(private_data_dir, 'project')
if not os.path.exists(project_dir):
os.mkdir(project_dir)
def build_private_data_files(self, instance, private_data_dir):
"""
Creates temporary files containing the private data.
@@ -257,9 +261,9 @@ class BaseTask(object):
# Instead, ssh private key file is explicitly passed via an
# env variable.
else:
private_data_files['credentials'][credential] = self.write_private_data_file(private_data_dir, None, data, 'env')
private_data_files['credentials'][credential] = self.write_private_data_file(private_data_dir, None, data, sub_dir='env')
for credential, data in private_data.get('certificates', {}).items():
self.write_private_data_file(private_data_dir, 'ssh_key_data-cert.pub', data, 'artifacts')
self.write_private_data_file(private_data_dir, 'ssh_key_data-cert.pub', data, sub_dir=os.path.join('artifacts', str(self.instance.id)))
return private_data_files, ssh_key_data
def build_passwords(self, instance, runtime_passwords):
@@ -282,7 +286,7 @@ class BaseTask(object):
content = yaml.safe_dump(vars)
else:
content = safe_dump(vars, safe_dict)
return self.write_private_data_file(private_data_dir, 'extravars', content, 'env')
return self.write_private_data_file(private_data_dir, 'extravars', content, sub_dir='env')
def add_awx_venv(self, env):
env['VIRTUAL_ENV'] = settings.AWX_VENV_PATH
@@ -321,13 +325,13 @@ class BaseTask(object):
# so we can associate emitted events to Host objects
self.runner_callback.host_map = {hostname: hv.pop('remote_tower_id', '') for hostname, hv in script_data.get('_meta', {}).get('hostvars', {}).items()}
file_content = '#! /usr/bin/env python3\n# -*- coding: utf-8 -*-\nprint(%r)\n' % json.dumps(script_data)
return self.write_private_data_file(private_data_dir, 'hosts', file_content, 'inventory', 0o700)
return self.write_private_data_file(private_data_dir, 'hosts', file_content, sub_dir='inventory', file_permissions=0o700)
def build_args(self, instance, private_data_dir, passwords):
raise NotImplementedError
def write_args_file(self, private_data_dir, args):
return self.write_private_data_file(private_data_dir, 'cmdline', ansible_runner.utils.args2cmdline(*args), 'env')
return self.write_private_data_file(private_data_dir, 'cmdline', ansible_runner.utils.args2cmdline(*args), sub_dir='env')
def build_credentials_list(self, instance):
return []
@@ -357,12 +361,65 @@ class BaseTask(object):
expect_passwords[k] = passwords.get(v, '') or ''
return expect_passwords
def release_lock(self, project):
try:
fcntl.lockf(self.lock_fd, fcntl.LOCK_UN)
except IOError as e:
logger.error("I/O error({0}) while trying to release lock file [{1}]: {2}".format(e.errno, project.get_lock_file(), e.strerror))
os.close(self.lock_fd)
raise
os.close(self.lock_fd)
self.lock_fd = None
def acquire_lock(self, project, unified_job_id=None):
if not os.path.exists(settings.PROJECTS_ROOT):
os.mkdir(settings.PROJECTS_ROOT)
lock_path = project.get_lock_file()
if lock_path is None:
# If from migration or someone blanked local_path for any other reason, recoverable by save
project.save()
lock_path = project.get_lock_file()
if lock_path is None:
raise RuntimeError(u'Invalid lock file path')
try:
self.lock_fd = os.open(lock_path, os.O_RDWR | os.O_CREAT)
except OSError as e:
logger.error("I/O error({0}) while trying to open lock file [{1}]: {2}".format(e.errno, lock_path, e.strerror))
raise
start_time = time.time()
while True:
try:
fcntl.lockf(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
break
except IOError as e:
if e.errno not in (errno.EAGAIN, errno.EACCES):
os.close(self.lock_fd)
logger.error("I/O error({0}) while trying to aquire lock on file [{1}]: {2}".format(e.errno, lock_path, e.strerror))
raise
else:
time.sleep(1.0)
self.instance.refresh_from_db(fields=['cancel_flag'])
if self.instance.cancel_flag or signal_callback():
logger.debug(f"Unified job {self.instance.id} was canceled while waiting for project file lock")
return
waiting_time = time.time() - start_time
if waiting_time > 1.0:
logger.info(f'Job {unified_job_id} waited {waiting_time} to acquire lock for local source tree for path {lock_path}.')
def pre_run_hook(self, instance, private_data_dir):
"""
Hook for any steps to run before the job/task starts
"""
instance.log_lifecycle("pre_run")
# Before task is started, ensure that job_event partitions exist
create_partition(instance.event_class._meta.db_table, start=instance.created)
def post_run_hook(self, instance, status):
"""
Hook for any steps to run before job/task is marked as complete.
@@ -375,15 +432,9 @@ class BaseTask(object):
"""
instance.log_lifecycle("finalize_run")
artifact_dir = os.path.join(private_data_dir, 'artifacts', str(self.instance.id))
job_profiling_dir = os.path.join(artifact_dir, 'playbook_profiling')
awx_profiling_dir = '/var/log/tower/playbook_profiling/'
collections_info = os.path.join(artifact_dir, 'collections.json')
ansible_version_file = os.path.join(artifact_dir, 'ansible_version.txt')
if not os.path.exists(awx_profiling_dir):
os.mkdir(awx_profiling_dir)
if os.path.isdir(job_profiling_dir):
shutil.copytree(job_profiling_dir, os.path.join(awx_profiling_dir, str(instance.pk)))
if os.path.exists(collections_info):
with open(collections_info) as ee_json_info:
ee_collections_info = json.loads(ee_json_info.read())
@@ -396,11 +447,17 @@ class BaseTask(object):
instance.save(update_fields=['ansible_version'])
@with_path_cleanup
@with_signal_handling
def run(self, pk, **kwargs):
"""
Run the job/task and capture its output.
"""
self.instance = self.model.objects.get(pk=pk)
if self.instance.status != 'canceled' and self.instance.cancel_flag:
self.instance = self.update_model(self.instance.pk, start_args='', status='canceled')
if self.instance.status not in ACTIVE_STATES:
# Prevent starting the job if it has been reaped or handled by another process.
raise RuntimeError(f'Not starting {self.instance.status} task pk={pk} because {self.instance.status} is not a valid active state')
if self.instance.execution_environment_id is None:
from awx.main.signals import disable_activity_stream
@@ -412,7 +469,6 @@ class BaseTask(object):
self.instance = self.update_model(pk, status='running', start_args='') # blank field to remove encrypted passwords
self.instance.websocket_emit_status("running")
status, rc = 'error', None
extra_update_fields = {}
fact_modification_times = {}
self.runner_callback.event_ct = 0
@@ -427,9 +483,11 @@ class BaseTask(object):
self.instance.send_notification_templates("running")
private_data_dir = self.build_private_data_dir(self.instance)
self.pre_run_hook(self.instance, private_data_dir)
self.build_project_dir(self.instance, private_data_dir)
self.instance.log_lifecycle("preparing_playbook")
if self.instance.cancel_flag:
if self.instance.cancel_flag or signal_callback():
self.instance = self.update_model(self.instance.pk, status='canceled')
if self.instance.status != 'running':
# Stop the task chain and prevent starting the job if it has
# already been canceled.
@@ -523,7 +581,7 @@ class BaseTask(object):
runner_settings['idle_timeout'] = idle_timeout
# Write out our own settings file
self.write_private_data_file(private_data_dir, 'settings', json.dumps(runner_settings), 'env')
self.write_private_data_file(private_data_dir, 'settings', json.dumps(runner_settings), sub_dir='env')
self.instance.log_lifecycle("running_playbook")
if isinstance(self.instance, SystemJob):
@@ -532,7 +590,7 @@ class BaseTask(object):
event_handler=self.runner_callback.event_handler,
finished_callback=self.runner_callback.finished_callback,
status_handler=self.runner_callback.status_handler,
cancel_callback=self.runner_callback.cancel_callback,
cancel_callback=signal_callback,
**params,
)
else:
@@ -547,20 +605,23 @@ class BaseTask(object):
rc = res.rc
if status in ('timeout', 'error'):
job_explanation = f"Job terminated due to {status}"
self.instance.job_explanation = self.instance.job_explanation or job_explanation
self.runner_callback.delay_update(skip_if_already_set=True, job_explanation=f"Job terminated due to {status}")
if status == 'timeout':
status = 'failed'
extra_update_fields['job_explanation'] = self.instance.job_explanation
# ensure failure notification sends even if playbook_on_stats event is not triggered
handle_success_and_failure_notifications.apply_async([self.instance.id])
elif status == 'canceled':
self.instance = self.update_model(pk)
cancel_flag_value = getattr(self.instance, 'cancel_flag', False)
if (cancel_flag_value is False) and signal_callback():
self.runner_callback.delay_update(skip_if_already_set=True, job_explanation="Task was canceled due to receiving a shutdown signal.")
status = 'failed'
elif cancel_flag_value is False:
self.runner_callback.delay_update(skip_if_already_set=True, job_explanation="The running ansible process received a shutdown signal.")
status = 'failed'
except ReceptorNodeNotFound as exc:
extra_update_fields['job_explanation'] = str(exc)
self.runner_callback.delay_update(job_explanation=str(exc))
except Exception:
# this could catch programming or file system errors
extra_update_fields['result_traceback'] = traceback.format_exc()
self.runner_callback.delay_update(result_traceback=traceback.format_exc())
logger.exception('%s Exception occurred while running task', self.instance.log_format)
finally:
logger.debug('%s finished running, producing %s events.', self.instance.log_format, self.runner_callback.event_ct)
@@ -570,18 +631,19 @@ class BaseTask(object):
except PostRunError as exc:
if status == 'successful':
status = exc.status
extra_update_fields['job_explanation'] = exc.args[0]
self.runner_callback.delay_update(job_explanation=exc.args[0])
if exc.tb:
extra_update_fields['result_traceback'] = exc.tb
self.runner_callback.delay_update(result_traceback=exc.tb)
except Exception:
logger.exception('{} Post run hook errored.'.format(self.instance.log_format))
# We really shouldn't get into this one but just in case....
if 'got an unexpected keyword argument' in extra_update_fields.get('result_traceback', ''):
extra_update_fields['result_traceback'] = "{}\n\n{}".format(extra_update_fields['result_traceback'], ANSIBLE_RUNNER_NEEDS_UPDATE_MESSAGE)
self.instance = self.update_model(pk)
self.instance = self.update_model(pk, status=status, emitted_events=self.runner_callback.event_ct, **extra_update_fields)
self.instance = self.update_model(pk, status=status, select_for_update=True, **self.runner_callback.get_delayed_update_fields())
# Field host_status_counts is used as a metric to check if event processing is finished
# we send notifications if it is, if not, callback receiver will send them
if (self.instance.host_status_counts is not None) or (not self.runner_callback.wrapup_event_dispatched):
self.instance.send_notification_templates('succeeded' if status == 'successful' else 'failed')
try:
self.final_run_hook(self.instance, status, private_data_dir, fact_modification_times)
@@ -596,8 +658,143 @@ class BaseTask(object):
raise AwxTaskError.TaskError(self.instance, rc)
class SourceControlMixin(BaseTask):
"""Utility methods for tasks that run use content from source control"""
def get_sync_needs(self, project, scm_branch=None):
project_path = project.get_project_path(check_if_exists=False)
job_revision = project.scm_revision
sync_needs = []
source_update_tag = 'update_{}'.format(project.scm_type)
branch_override = bool(scm_branch and scm_branch != project.scm_branch)
# TODO: skip syncs for inventory updates. Now, UI needs a link added so clients can link to project
# source_project is only a field on inventory sources.
if isinstance(self.instance, InventoryUpdate):
sync_needs.append(source_update_tag)
elif not project.scm_type:
pass # manual projects are not synced, user has responsibility for that
elif not os.path.exists(project_path):
logger.debug(f'Performing fresh clone of {project.id} for unified job {self.instance.id} on this instance.')
sync_needs.append(source_update_tag)
elif project.scm_type == 'git' and project.scm_revision and (not branch_override):
try:
git_repo = git.Repo(project_path)
if job_revision == git_repo.head.commit.hexsha:
logger.debug(f'Skipping project sync for {self.instance.id} because commit is locally available')
else:
sync_needs.append(source_update_tag)
except (ValueError, BadGitName, git.exc.InvalidGitRepositoryError):
logger.debug(f'Needed commit for {self.instance.id} not in local source tree, will sync with remote')
sync_needs.append(source_update_tag)
else:
logger.debug(f'Project not available locally, {self.instance.id} will sync with remote')
sync_needs.append(source_update_tag)
has_cache = os.path.exists(os.path.join(project.get_cache_path(), project.cache_id))
# Galaxy requirements are not supported for manual projects
if project.scm_type and ((not has_cache) or branch_override):
sync_needs.extend(['install_roles', 'install_collections'])
return sync_needs
def spawn_project_sync(self, project, sync_needs, scm_branch=None):
pu_ig = self.instance.instance_group
pu_en = Instance.objects.me().hostname
sync_metafields = dict(
launch_type="sync",
job_type='run',
job_tags=','.join(sync_needs),
status='running',
instance_group=pu_ig,
execution_node=pu_en,
controller_node=pu_en,
celery_task_id=self.instance.celery_task_id,
)
if scm_branch and scm_branch != project.scm_branch:
sync_metafields['scm_branch'] = scm_branch
sync_metafields['scm_clean'] = True # to accomidate force pushes
if 'update_' not in sync_metafields['job_tags']:
sync_metafields['scm_revision'] = project.scm_revision
local_project_sync = project.create_project_update(_eager_fields=sync_metafields)
local_project_sync.log_lifecycle("controller_node_chosen")
local_project_sync.log_lifecycle("execution_node_chosen")
return local_project_sync
def sync_and_copy_without_lock(self, project, private_data_dir, scm_branch=None):
sync_needs = self.get_sync_needs(project, scm_branch=scm_branch)
if sync_needs:
local_project_sync = self.spawn_project_sync(project, sync_needs, scm_branch=scm_branch)
# save the associated job before calling run() so that a
# cancel() call on the job can cancel the project update
if isinstance(self.instance, Job):
self.instance = self.update_model(self.instance.pk, project_update=local_project_sync)
else:
self.instance = self.update_model(self.instance.pk, source_project_update=local_project_sync)
try:
# the job private_data_dir is passed so sync can download roles and collections there
sync_task = RunProjectUpdate(job_private_data_dir=private_data_dir)
sync_task.run(local_project_sync.id)
local_project_sync.refresh_from_db()
if isinstance(self.instance, Job):
self.instance = self.update_model(self.instance.pk, scm_revision=local_project_sync.scm_revision)
except Exception:
local_project_sync.refresh_from_db()
if local_project_sync.status != 'canceled':
self.instance = self.update_model(
self.instance.pk,
status='failed',
job_explanation=(
'Previous Task Failed: {"job_type": "project_update", '
f'"job_name": "{local_project_sync.name}", "job_id": "{local_project_sync.id}"}}'
),
)
raise
self.instance.refresh_from_db()
if self.instance.cancel_flag:
return
else:
# Case where a local sync is not needed, meaning that local tree is
# up-to-date with project, job is running project current version
if isinstance(self.instance, Job):
self.instance = self.update_model(self.instance.pk, scm_revision=project.scm_revision)
# Project update does not copy the folder, so copy here
RunProjectUpdate.make_local_copy(project, private_data_dir)
def sync_and_copy(self, project, private_data_dir, scm_branch=None):
self.acquire_lock(project, self.instance.id)
try:
original_branch = None
project_path = project.get_project_path(check_if_exists=False)
if project.scm_type == 'git' and (scm_branch and scm_branch != project.scm_branch):
if os.path.exists(project_path):
git_repo = git.Repo(project_path)
if git_repo.head.is_detached:
original_branch = git_repo.head.commit
else:
original_branch = git_repo.active_branch
return self.sync_and_copy_without_lock(project, private_data_dir, scm_branch=scm_branch)
finally:
# We have made the copy so we can set the tree back to its normal state
if original_branch:
# for git project syncs, non-default branches can be problems
# restore to branch the repo was on before this run
try:
original_branch.checkout()
except Exception:
# this could have failed due to dirty tree, but difficult to predict all cases
logger.exception(f'Failed to restore project repo to prior state after {self.instance.id}')
self.release_lock(project)
@task(queue=get_local_queuename)
class RunJob(BaseTask):
class RunJob(SourceControlMixin, BaseTask):
"""
Run a job using ansible-playbook.
"""
@@ -866,98 +1063,14 @@ class RunJob(BaseTask):
job = self.update_model(job.pk, status='failed', job_explanation=msg)
raise RuntimeError(msg)
project_path = job.project.get_project_path(check_if_exists=False)
job_revision = job.project.scm_revision
sync_needs = []
source_update_tag = 'update_{}'.format(job.project.scm_type)
branch_override = bool(job.scm_branch and job.scm_branch != job.project.scm_branch)
if not job.project.scm_type:
pass # manual projects are not synced, user has responsibility for that
elif not os.path.exists(project_path):
logger.debug('Performing fresh clone of {} on this instance.'.format(job.project))
sync_needs.append(source_update_tag)
elif job.project.scm_type == 'git' and job.project.scm_revision and (not branch_override):
try:
git_repo = git.Repo(project_path)
if job_revision == git_repo.head.commit.hexsha:
logger.debug('Skipping project sync for {} because commit is locally available'.format(job.log_format))
else:
sync_needs.append(source_update_tag)
except (ValueError, BadGitName, git.exc.InvalidGitRepositoryError):
logger.debug('Needed commit for {} not in local source tree, will sync with remote'.format(job.log_format))
sync_needs.append(source_update_tag)
else:
logger.debug('Project not available locally, {} will sync with remote'.format(job.log_format))
sync_needs.append(source_update_tag)
has_cache = os.path.exists(os.path.join(job.project.get_cache_path(), job.project.cache_id))
# Galaxy requirements are not supported for manual projects
if job.project.scm_type and ((not has_cache) or branch_override):
sync_needs.extend(['install_roles', 'install_collections'])
if sync_needs:
pu_ig = job.instance_group
pu_en = Instance.objects.me().hostname
sync_metafields = dict(
launch_type="sync",
job_type='run',
job_tags=','.join(sync_needs),
status='running',
instance_group=pu_ig,
execution_node=pu_en,
controller_node=pu_en,
celery_task_id=job.celery_task_id,
)
if branch_override:
sync_metafields['scm_branch'] = job.scm_branch
sync_metafields['scm_clean'] = True # to accomidate force pushes
if 'update_' not in sync_metafields['job_tags']:
sync_metafields['scm_revision'] = job_revision
local_project_sync = job.project.create_project_update(_eager_fields=sync_metafields)
local_project_sync.log_lifecycle("controller_node_chosen")
local_project_sync.log_lifecycle("execution_node_chosen")
create_partition(local_project_sync.event_class._meta.db_table, start=local_project_sync.created)
# save the associated job before calling run() so that a
# cancel() call on the job can cancel the project update
job = self.update_model(job.pk, project_update=local_project_sync)
project_update_task = local_project_sync._get_task_class()
try:
# the job private_data_dir is passed so sync can download roles and collections there
sync_task = project_update_task(job_private_data_dir=private_data_dir)
sync_task.run(local_project_sync.id)
local_project_sync.refresh_from_db()
job = self.update_model(job.pk, scm_revision=local_project_sync.scm_revision)
except Exception:
local_project_sync.refresh_from_db()
if local_project_sync.status != 'canceled':
job = self.update_model(
job.pk,
status='failed',
job_explanation=(
'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}'
% ('project_update', local_project_sync.name, local_project_sync.id)
),
)
raise
job.refresh_from_db()
if job.cancel_flag:
return
else:
# Case where a local sync is not needed, meaning that local tree is
# up-to-date with project, job is running project current version
if job_revision:
job = self.update_model(job.pk, scm_revision=job_revision)
# Project update does not copy the folder, so copy here
RunProjectUpdate.make_local_copy(job.project, private_data_dir, scm_revision=job_revision)
if job.inventory.kind == 'smart':
# cache smart inventory memberships so that the host_filter query is not
# ran inside of the event saving code
update_smart_memberships_for_inventory(job.inventory)
def build_project_dir(self, job, private_data_dir):
self.sync_and_copy(job.project, private_data_dir, scm_branch=job.scm_branch)
def final_run_hook(self, job, status, private_data_dir, fact_modification_times):
super(RunJob, self).final_run_hook(job, status, private_data_dir, fact_modification_times)
if not private_data_dir:
@@ -989,7 +1102,6 @@ class RunProjectUpdate(BaseTask):
def __init__(self, *args, job_private_data_dir=None, **kwargs):
super(RunProjectUpdate, self).__init__(*args, **kwargs)
self.original_branch = None
self.job_private_data_dir = job_private_data_dir
def build_private_data(self, project_update, private_data_dir):
@@ -1159,6 +1271,10 @@ class RunProjectUpdate(BaseTask):
# for raw archive, prevent error moving files between volumes
extra_vars['ansible_remote_tmp'] = os.path.join(project_update.get_project_path(check_if_exists=False), '.ansible_awx', 'tmp')
if project_update.project.signature_validation_credential is not None:
pubkey = project_update.project.signature_validation_credential.get_input('gpg_public_key')
extra_vars['gpg_pubkey'] = pubkey
self._write_extra_vars_file(private_data_dir, extra_vars)
def build_playbook_path_relative_to_cwd(self, project_update, private_data_dir):
@@ -1176,132 +1292,13 @@ class RunProjectUpdate(BaseTask):
d[r'^Are you sure you want to continue connecting \(yes/no\)\?\s*?$'] = 'yes'
return d
def _update_dependent_inventories(self, project_update, dependent_inventory_sources):
scm_revision = project_update.project.scm_revision
inv_update_class = InventoryUpdate._get_task_class()
for inv_src in dependent_inventory_sources:
if not inv_src.update_on_project_update:
continue
if inv_src.scm_last_revision == scm_revision:
logger.debug('Skipping SCM inventory update for `{}` because ' 'project has not changed.'.format(inv_src.name))
continue
logger.debug('Local dependent inventory update for `{}`.'.format(inv_src.name))
with transaction.atomic():
if InventoryUpdate.objects.filter(inventory_source=inv_src, status__in=ACTIVE_STATES).exists():
logger.debug('Skipping SCM inventory update for `{}` because ' 'another update is already active.'.format(inv_src.name))
continue
if settings.IS_K8S:
instance_group = InventoryUpdate(inventory_source=inv_src).preferred_instance_groups[0]
else:
instance_group = project_update.instance_group
local_inv_update = inv_src.create_inventory_update(
_eager_fields=dict(
launch_type='scm',
status='running',
instance_group=instance_group,
execution_node=project_update.execution_node,
controller_node=project_update.execution_node,
source_project_update=project_update,
celery_task_id=project_update.celery_task_id,
)
)
local_inv_update.log_lifecycle("controller_node_chosen")
local_inv_update.log_lifecycle("execution_node_chosen")
try:
create_partition(local_inv_update.event_class._meta.db_table, start=local_inv_update.created)
inv_update_class().run(local_inv_update.id)
except Exception:
logger.exception('{} Unhandled exception updating dependent SCM inventory sources.'.format(project_update.log_format))
try:
project_update.refresh_from_db()
except ProjectUpdate.DoesNotExist:
logger.warning('Project update deleted during updates of dependent SCM inventory sources.')
break
try:
local_inv_update.refresh_from_db()
except InventoryUpdate.DoesNotExist:
logger.warning('%s Dependent inventory update deleted during execution.', project_update.log_format)
continue
if project_update.cancel_flag:
logger.info('Project update {} was canceled while updating dependent inventories.'.format(project_update.log_format))
break
if local_inv_update.cancel_flag:
logger.info('Continuing to process project dependencies after {} was canceled'.format(local_inv_update.log_format))
if local_inv_update.status == 'successful':
inv_src.scm_last_revision = scm_revision
inv_src.save(update_fields=['scm_last_revision'])
def release_lock(self, instance):
try:
fcntl.lockf(self.lock_fd, fcntl.LOCK_UN)
except IOError as e:
logger.error("I/O error({0}) while trying to release lock file [{1}]: {2}".format(e.errno, instance.get_lock_file(), e.strerror))
os.close(self.lock_fd)
raise
os.close(self.lock_fd)
self.lock_fd = None
'''
Note: We don't support blocking=False
'''
def acquire_lock(self, instance, blocking=True):
lock_path = instance.get_lock_file()
if lock_path is None:
# If from migration or someone blanked local_path for any other reason, recoverable by save
instance.save()
lock_path = instance.get_lock_file()
if lock_path is None:
raise RuntimeError(u'Invalid lock file path')
try:
self.lock_fd = os.open(lock_path, os.O_RDWR | os.O_CREAT)
except OSError as e:
logger.error("I/O error({0}) while trying to open lock file [{1}]: {2}".format(e.errno, lock_path, e.strerror))
raise
start_time = time.time()
while True:
try:
instance.refresh_from_db(fields=['cancel_flag'])
if instance.cancel_flag:
logger.debug("ProjectUpdate({0}) was canceled".format(instance.pk))
return
fcntl.lockf(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
break
except IOError as e:
if e.errno not in (errno.EAGAIN, errno.EACCES):
os.close(self.lock_fd)
logger.error("I/O error({0}) while trying to aquire lock on file [{1}]: {2}".format(e.errno, lock_path, e.strerror))
raise
else:
time.sleep(1.0)
waiting_time = time.time() - start_time
if waiting_time > 1.0:
logger.info('{} spent {} waiting to acquire lock for local source tree ' 'for path {}.'.format(instance.log_format, waiting_time, lock_path))
def pre_run_hook(self, instance, private_data_dir):
super(RunProjectUpdate, self).pre_run_hook(instance, private_data_dir)
# re-create root project folder if a natural disaster has destroyed it
if not os.path.exists(settings.PROJECTS_ROOT):
os.mkdir(settings.PROJECTS_ROOT)
project_path = instance.project.get_project_path(check_if_exists=False)
self.acquire_lock(instance)
self.original_branch = None
if instance.scm_type == 'git' and instance.branch_override:
if os.path.exists(project_path):
git_repo = git.Repo(project_path)
if git_repo.head.is_detached:
self.original_branch = git_repo.head.commit
else:
self.original_branch = git_repo.active_branch
if instance.launch_type != 'sync':
self.acquire_lock(instance.project, instance.id)
if not os.path.exists(project_path):
os.makedirs(project_path) # used as container mount
@@ -1312,11 +1309,12 @@ class RunProjectUpdate(BaseTask):
shutil.rmtree(stage_path)
os.makedirs(stage_path) # presence of empty cache indicates lack of roles or collections
def build_project_dir(self, instance, private_data_dir):
# the project update playbook is not in a git repo, but uses a vendoring directory
# to be consistent with the ansible-runner model,
# that is moved into the runner project folder here
awx_playbooks = self.get_path_to('../../', 'playbooks')
copy_tree(awx_playbooks, os.path.join(private_data_dir, 'project'))
shutil.copytree(awx_playbooks, os.path.join(private_data_dir, 'project'))
@staticmethod
def clear_project_cache(cache_dir, keep_value):
@@ -1333,50 +1331,18 @@ class RunProjectUpdate(BaseTask):
logger.warning(f"Could not remove cache directory {old_path}")
@staticmethod
def make_local_copy(p, job_private_data_dir, scm_revision=None):
def make_local_copy(project, job_private_data_dir):
"""Copy project content (roles and collections) to a job private_data_dir
:param object p: Either a project or a project update
:param object project: Either a project or a project update
:param str job_private_data_dir: The root of the target ansible-runner folder
:param str scm_revision: For branch_override cases, the git revision to copy
"""
project_path = p.get_project_path(check_if_exists=False)
project_path = project.get_project_path(check_if_exists=False)
destination_folder = os.path.join(job_private_data_dir, 'project')
if not scm_revision:
scm_revision = p.scm_revision
if p.scm_type == 'git':
git_repo = git.Repo(project_path)
if not os.path.exists(destination_folder):
os.mkdir(destination_folder, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
tmp_branch_name = 'awx_internal/{}'.format(uuid4())
# always clone based on specific job revision
if not p.scm_revision:
raise RuntimeError('Unexpectedly could not determine a revision to run from project.')
source_branch = git_repo.create_head(tmp_branch_name, p.scm_revision)
# git clone must take file:// syntax for source repo or else options like depth will be ignored
source_as_uri = Path(project_path).as_uri()
git.Repo.clone_from(
source_as_uri,
destination_folder,
branch=source_branch,
depth=1,
single_branch=True, # shallow, do not copy full history
)
# submodules copied in loop because shallow copies from local HEADs are ideal
# and no git clone submodule options are compatible with minimum requirements
for submodule in git_repo.submodules:
subrepo_path = os.path.abspath(os.path.join(project_path, submodule.path))
subrepo_destination_folder = os.path.abspath(os.path.join(destination_folder, submodule.path))
subrepo_uri = Path(subrepo_path).as_uri()
git.Repo.clone_from(subrepo_uri, subrepo_destination_folder, depth=1, single_branch=True)
# force option is necessary because remote refs are not counted, although no information is lost
git_repo.delete_head(tmp_branch_name, force=True)
else:
copy_tree(project_path, destination_folder, preserve_symlinks=1)
shutil.copytree(project_path, destination_folder, ignore=shutil.ignore_patterns('.git'), symlinks=True)
# copy over the roles and collection cache to job folder
cache_path = os.path.join(p.get_cache_path(), p.cache_id)
cache_path = os.path.join(project.get_cache_path(), project.cache_id)
subfolders = []
if settings.AWX_COLLECTIONS_ENABLED:
subfolders.append('requirements_collections')
@@ -1386,8 +1352,8 @@ class RunProjectUpdate(BaseTask):
cache_subpath = os.path.join(cache_path, subfolder)
if os.path.exists(cache_subpath):
dest_subpath = os.path.join(job_private_data_dir, subfolder)
copy_tree(cache_subpath, dest_subpath, preserve_symlinks=1)
logger.debug('{0} {1} prepared {2} from cache'.format(type(p).__name__, p.pk, dest_subpath))
shutil.copytree(cache_subpath, dest_subpath, symlinks=True)
logger.debug('{0} {1} prepared {2} from cache'.format(type(project).__name__, project.pk, dest_subpath))
def post_run_hook(self, instance, status):
super(RunProjectUpdate, self).post_run_hook(instance, status)
@@ -1417,23 +1383,13 @@ class RunProjectUpdate(BaseTask):
if self.job_private_data_dir:
if status == 'successful':
# copy project folder before resetting to default branch
# because some git-tree-specific resources (like submodules) might matter
self.make_local_copy(instance, self.job_private_data_dir)
if self.original_branch:
# for git project syncs, non-default branches can be problems
# restore to branch the repo was on before this run
try:
self.original_branch.checkout()
except Exception:
# this could have failed due to dirty tree, but difficult to predict all cases
logger.exception('Failed to restore project repo to prior state after {}'.format(instance.log_format))
finally:
self.release_lock(instance)
if instance.launch_type != 'sync':
self.release_lock(instance.project)
p = instance.project
if instance.job_type == 'check' and status not in (
'failed',
'canceled',
):
if instance.job_type == 'check' and status not in ('failed', 'canceled'):
if self.runner_callback.playbook_new_revision:
p.scm_revision = self.runner_callback.playbook_new_revision
else:
@@ -1443,12 +1399,6 @@ class RunProjectUpdate(BaseTask):
p.inventory_files = p.inventories
p.save(update_fields=['scm_revision', 'playbook_files', 'inventory_files'])
# Update any inventories that depend on this project
dependent_inventory_sources = p.scm_inventory_sources.filter(update_on_project_update=True)
if len(dependent_inventory_sources) > 0:
if status == 'successful' and instance.launch_type != 'sync':
self._update_dependent_inventories(instance, dependent_inventory_sources)
def build_execution_environment_params(self, instance, private_data_dir):
if settings.IS_K8S:
return {}
@@ -1459,15 +1409,15 @@ class RunProjectUpdate(BaseTask):
params.setdefault('container_volume_mounts', [])
params['container_volume_mounts'].extend(
[
f"{project_path}:{project_path}:Z",
f"{cache_path}:{cache_path}:Z",
f"{project_path}:{project_path}:z",
f"{cache_path}:{cache_path}:z",
]
)
return params
@task(queue=get_local_queuename)
class RunInventoryUpdate(BaseTask):
class RunInventoryUpdate(SourceControlMixin, BaseTask):
model = InventoryUpdate
event_model = InventoryUpdateEvent
@@ -1609,7 +1559,7 @@ class RunInventoryUpdate(BaseTask):
if injector is not None:
content = injector.inventory_contents(inventory_update, private_data_dir)
# must be a statically named file
self.write_private_data_file(private_data_dir, injector.filename, content, 'inventory', 0o700)
self.write_private_data_file(private_data_dir, injector.filename, content, sub_dir='inventory', file_permissions=0o700)
rel_path = os.path.join('inventory', injector.filename)
elif src == 'scm':
rel_path = os.path.join('project', inventory_update.source_path)
@@ -1623,61 +1573,18 @@ class RunInventoryUpdate(BaseTask):
# All credentials not used by inventory source injector
return inventory_update.get_extra_credentials()
def pre_run_hook(self, inventory_update, private_data_dir):
super(RunInventoryUpdate, self).pre_run_hook(inventory_update, private_data_dir)
def build_project_dir(self, inventory_update, private_data_dir):
source_project = None
if inventory_update.inventory_source:
source_project = inventory_update.inventory_source.source_project
if (
inventory_update.source == 'scm' and inventory_update.launch_type != 'scm' and source_project and source_project.scm_type
): # never ever update manual projects
# Check if the content cache exists, so that we do not unnecessarily re-download roles
sync_needs = ['update_{}'.format(source_project.scm_type)]
has_cache = os.path.exists(os.path.join(source_project.get_cache_path(), source_project.cache_id))
# Galaxy requirements are not supported for manual projects
if not has_cache:
sync_needs.extend(['install_roles', 'install_collections'])
local_project_sync = source_project.create_project_update(
_eager_fields=dict(
launch_type="sync",
job_type='run',
job_tags=','.join(sync_needs),
status='running',
execution_node=Instance.objects.me().hostname,
controller_node=Instance.objects.me().hostname,
instance_group=inventory_update.instance_group,
celery_task_id=inventory_update.celery_task_id,
)
)
local_project_sync.log_lifecycle("controller_node_chosen")
local_project_sync.log_lifecycle("execution_node_chosen")
create_partition(local_project_sync.event_class._meta.db_table, start=local_project_sync.created)
# associate the inventory update before calling run() so that a
# cancel() call on the inventory update can cancel the project update
local_project_sync.scm_inventory_updates.add(inventory_update)
project_update_task = local_project_sync._get_task_class()
try:
sync_task = project_update_task(job_private_data_dir=private_data_dir)
sync_task.run(local_project_sync.id)
local_project_sync.refresh_from_db()
inventory_update.inventory_source.scm_last_revision = local_project_sync.scm_revision
inventory_update.inventory_source.save(update_fields=['scm_last_revision'])
except Exception:
inventory_update = self.update_model(
inventory_update.pk,
status='failed',
job_explanation=(
'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}'
% ('project_update', local_project_sync.name, local_project_sync.id)
),
)
raise
elif inventory_update.source == 'scm' and inventory_update.launch_type == 'scm' and source_project:
# This follows update, not sync, so make copy here
RunProjectUpdate.make_local_copy(source_project, private_data_dir)
if inventory_update.source == 'scm':
if not source_project:
raise RuntimeError('Could not find project to run SCM inventory update from.')
self.sync_and_copy(source_project, private_data_dir)
else:
# If source is not SCM make an empty project directory, content is built inside inventory folder
super(RunInventoryUpdate, self).build_project_dir(inventory_update, private_data_dir)
def post_run_hook(self, inventory_update, status):
super(RunInventoryUpdate, self).post_run_hook(inventory_update, status)
@@ -1720,7 +1627,7 @@ class RunInventoryUpdate(BaseTask):
handler = SpecialInventoryHandler(
self.runner_callback.event_handler,
self.runner_callback.cancel_callback,
signal_callback,
verbosity=inventory_update.verbosity,
job_timeout=self.get_instance_timeout(self.instance),
start_time=inventory_update.started,

View File

@@ -12,6 +12,7 @@ import yaml
# Django
from django.conf import settings
from django.db import connections
# Runner
import ansible_runner
@@ -24,10 +25,8 @@ from awx.main.utils.common import (
parse_yaml_or_json,
cleanup_new_process,
)
from awx.main.constants import (
MAX_ISOLATED_PATH_COLON_DELIMITER,
ANSIBLE_RUNNER_NEEDS_UPDATE_MESSAGE,
)
from awx.main.constants import MAX_ISOLATED_PATH_COLON_DELIMITER
from awx.main.tasks.signals import signal_state, signal_callback, SignalExit
# Receptorctl
from receptorctl.socket_interface import ReceptorControl
@@ -102,16 +101,22 @@ def administrative_workunit_reaper(work_list=None):
for unit_id, work_data in work_list.items():
extra_data = work_data.get('ExtraData')
if (extra_data is None) or (extra_data.get('RemoteWorkType') != 'ansible-runner'):
if extra_data is None:
continue # if this is not ansible-runner work, we do not want to touch it
params = extra_data.get('RemoteParams', {}).get('params')
if not params:
continue
if not (params == '--worker-info' or params.startswith('cleanup')):
continue # if this is not a cleanup or health check, we do not want to touch it
if work_data.get('StateName') in RECEPTOR_ACTIVE_STATES:
continue # do not want to touch active work units
logger.info(f'Reaping orphaned work unit {unit_id} with params {params}')
if isinstance(extra_data, str):
if not work_data.get('StateName', None) or work_data.get('StateName') in RECEPTOR_ACTIVE_STATES:
continue
else:
if extra_data.get('RemoteWorkType') != 'ansible-runner':
continue
params = extra_data.get('RemoteParams', {}).get('params')
if not params:
continue
if not (params == '--worker-info' or params.startswith('cleanup')):
continue # if this is not a cleanup or health check, we do not want to touch it
if work_data.get('StateName') in RECEPTOR_ACTIVE_STATES:
continue # do not want to touch active work units
logger.info(f'Reaping orphaned work unit {unit_id} with params {params}')
receptor_ctl.simple_command(f"work release {unit_id}")
@@ -332,24 +337,37 @@ class AWXReceptorJob:
shutil.rmtree(artifact_dir)
resultsock, resultfile = receptor_ctl.get_work_results(self.unit_id, return_socket=True, return_sockfile=True)
# Both "processor" and "cancel_watcher" are spawned in separate threads.
# We wait for the first one to return. If cancel_watcher returns first,
# we yank the socket out from underneath the processor, which will cause it
# to exit. A reference to the processor_future is passed into the cancel_watcher_future,
# Which exits if the job has finished normally. The context manager ensures we do not
# leave any threads laying around.
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
processor_future = executor.submit(self.processor, resultfile)
cancel_watcher_future = executor.submit(self.cancel_watcher, processor_future)
futures = [processor_future, cancel_watcher_future]
first_future = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
res = list(first_future.done)[0].result()
if res.status == 'canceled':
connections.close_all()
# "processor" and the main thread will be separate threads.
# If a cancel happens, the main thread will encounter an exception, in which case
# we yank the socket out from underneath the processor, which will cause it to exit.
# The ThreadPoolExecutor context manager ensures we do not leave any threads laying around.
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
processor_future = executor.submit(self.processor, resultfile)
try:
signal_state.raise_exception = True
# address race condition where SIGTERM was issued after this dispatcher task started
if signal_callback():
raise SignalExit()
res = processor_future.result()
except SignalExit:
receptor_ctl.simple_command(f"work cancel {self.unit_id}")
resultsock.shutdown(socket.SHUT_RDWR)
resultfile.close()
elif res.status == 'error':
result = namedtuple('result', ['status', 'rc'])
res = result('canceled', 1)
finally:
signal_state.raise_exception = False
if res.status == 'error':
# If ansible-runner ran, but an error occured at runtime, the traceback information
# is saved via the status_handler passed in to the processor.
if 'result_traceback' in self.task.runner_callback.extra_update_fields:
return res
try:
unit_status = receptor_ctl.simple_command(f'work status {self.unit_id}')
detail = unit_status.get('Detail', None)
@@ -365,28 +383,19 @@ class AWXReceptorJob:
logger.warning(f"Could not launch pod for {log_name}. Exceeded quota.")
self.task.update_model(self.task.instance.pk, status='pending')
return
# If ansible-runner ran, but an error occured at runtime, the traceback information
# is saved via the status_handler passed in to the processor.
if state_name == 'Succeeded':
return res
if not self.task.instance.result_traceback:
try:
resultsock = receptor_ctl.get_work_results(self.unit_id, return_sockfile=True)
lines = resultsock.readlines()
receptor_output = b"".join(lines).decode()
if receptor_output:
self.task.instance.result_traceback = receptor_output
if 'got an unexpected keyword argument' in receptor_output:
self.task.instance.result_traceback = "{}\n\n{}".format(receptor_output, ANSIBLE_RUNNER_NEEDS_UPDATE_MESSAGE)
self.task.instance.save(update_fields=['result_traceback'])
elif detail:
self.task.instance.result_traceback = detail
self.task.instance.save(update_fields=['result_traceback'])
else:
logger.warning(f'No result details or output from {self.task.instance.log_format}, status:\n{state_name}')
except Exception:
raise RuntimeError(detail)
try:
resultsock = receptor_ctl.get_work_results(self.unit_id, return_sockfile=True)
lines = resultsock.readlines()
receptor_output = b"".join(lines).decode()
if receptor_output:
self.task.runner_callback.delay_update(result_traceback=receptor_output)
elif detail:
self.task.runner_callback.delay_update(result_traceback=detail)
else:
logger.warning(f'No result details or output from {self.task.instance.log_format}, status:\n{state_name}')
except Exception:
raise RuntimeError(detail)
return res
@@ -447,18 +456,6 @@ class AWXReceptorJob:
return 'local'
return 'ansible-runner'
@cleanup_new_process
def cancel_watcher(self, processor_future):
while True:
if processor_future.done():
return processor_future.result()
if self.task.runner_callback.cancel_callback():
result = namedtuple('result', ['status', 'rc'])
return result('canceled', 1)
time.sleep(1)
@property
def pod_definition(self):
ee = self.task.instance.execution_environment

71
awx/main/tasks/signals.py Normal file
View File

@@ -0,0 +1,71 @@
import signal
import functools
import logging
logger = logging.getLogger('awx.main.tasks.signals')
__all__ = ['with_signal_handling', 'signal_callback']
class SignalExit(Exception):
pass
class SignalState:
def reset(self):
self.sigterm_flag = False
self.is_active = False
self.original_sigterm = None
self.original_sigint = None
self.raise_exception = False
def __init__(self):
self.reset()
def set_flag(self, *args):
"""Method to pass into the python signal.signal method to receive signals"""
self.sigterm_flag = True
if self.raise_exception:
self.raise_exception = False # so it is not raised a second time in error handling
raise SignalExit()
def connect_signals(self):
self.original_sigterm = signal.getsignal(signal.SIGTERM)
self.original_sigint = signal.getsignal(signal.SIGINT)
signal.signal(signal.SIGTERM, self.set_flag)
signal.signal(signal.SIGINT, self.set_flag)
self.is_active = True
def restore_signals(self):
signal.signal(signal.SIGTERM, self.original_sigterm)
signal.signal(signal.SIGINT, self.original_sigint)
self.reset()
signal_state = SignalState()
def signal_callback():
return signal_state.sigterm_flag
def with_signal_handling(f):
"""
Change signal handling to make signal_callback return True in event of SIGTERM or SIGINT.
"""
@functools.wraps(f)
def _wrapped(*args, **kwargs):
try:
this_is_outermost_caller = False
if not signal_state.is_active:
signal_state.connect_signals()
this_is_outermost_caller = True
return f(*args, **kwargs)
finally:
if this_is_outermost_caller:
signal_state.restore_signals()
return _wrapped

View File

@@ -10,12 +10,13 @@ from contextlib import redirect_stdout
import shutil
import time
from distutils.version import LooseVersion as Version
from datetime import datetime
# Django
from django.conf import settings
from django.db import transaction, DatabaseError, IntegrityError
from django.db.models.fields.related import ForeignKey
from django.utils.timezone import now
from django.utils.timezone import now, timedelta
from django.utils.encoding import smart_str
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
@@ -53,7 +54,8 @@ from awx.main.dispatch import get_local_queuename, reaper
from awx.main.utils.common import (
ignore_inventory_computed_fields,
ignore_inventory_group_removal,
schedule_task_manager,
ScheduleWorkflowManager,
ScheduleTaskManager,
)
from awx.main.utils.external_logging import reconfigure_rsyslog
@@ -103,7 +105,10 @@ def dispatch_startup():
#
apply_cluster_membership_policies()
cluster_node_heartbeat()
Metrics().clear_values()
reaper.startup_reaping()
reaper.reap_waiting(grace_period=0)
m = Metrics()
m.reset_values()
# Update Tower's rsyslog.conf file based on loggins settings in the db
reconfigure_rsyslog()
@@ -114,9 +119,9 @@ def inform_cluster_of_shutdown():
this_inst = Instance.objects.get(hostname=settings.CLUSTER_HOST_ID)
this_inst.mark_offline(update_last_seen=True, errors=_('Instance received normal shutdown signal'))
try:
reaper.reap(this_inst)
reaper.reap_waiting(this_inst, grace_period=0)
except Exception:
logger.exception('failed to reap jobs for {}'.format(this_inst.hostname))
logger.exception('failed to reap waiting jobs for {}'.format(this_inst.hostname))
logger.warning('Normal shutdown signal for instance {}, ' 'removed self from capacity pool.'.format(this_inst.hostname))
except Exception:
logger.exception('Encountered problem with normal shutdown signal.')
@@ -478,8 +483,8 @@ def inspect_execution_nodes(instance_list):
execution_node_health_check.apply_async([hostname])
@task(queue=get_local_queuename)
def cluster_node_heartbeat():
@task(queue=get_local_queuename, bind_kwargs=['dispatch_time', 'worker_tasks'])
def cluster_node_heartbeat(dispatch_time=None, worker_tasks=None):
logger.debug("Cluster node heartbeat task.")
nowtime = now()
instance_list = list(Instance.objects.all())
@@ -502,12 +507,23 @@ def cluster_node_heartbeat():
if this_inst:
startup_event = this_inst.is_lost(ref_time=nowtime)
last_last_seen = this_inst.last_seen
this_inst.local_health_check()
if startup_event and this_inst.capacity != 0:
logger.warning('Rejoining the cluster as instance {}.'.format(this_inst.hostname))
logger.warning(f'Rejoining the cluster as instance {this_inst.hostname}. Prior last_seen {last_last_seen}')
return
elif not last_last_seen:
logger.warning(f'Instance does not have recorded last_seen, updating to {nowtime}')
elif (nowtime - last_last_seen) > timedelta(seconds=settings.CLUSTER_NODE_HEARTBEAT_PERIOD + 2):
logger.warning(f'Heartbeat skew - interval={(nowtime - last_last_seen).total_seconds():.4f}, expected={settings.CLUSTER_NODE_HEARTBEAT_PERIOD}')
else:
raise RuntimeError("Cluster Host Not Found: {}".format(settings.CLUSTER_HOST_ID))
if settings.AWX_AUTO_DEPROVISION_INSTANCES:
(changed, this_inst) = Instance.objects.register(ip_address=os.environ.get('MY_POD_IP'), node_type='control', uuid=settings.SYSTEM_UUID)
if changed:
logger.warning(f'Recreated instance record {this_inst.hostname} after unexpected removal')
this_inst.local_health_check()
else:
raise RuntimeError("Cluster Host Not Found: {}".format(settings.CLUSTER_HOST_ID))
# IFF any node has a greater version than we do, then we'll shutdown services
for other_inst in instance_list:
if other_inst.node_type in ('execution', 'hop'):
@@ -527,7 +543,9 @@ def cluster_node_heartbeat():
for other_inst in lost_instances:
try:
reaper.reap(other_inst)
explanation = "Job reaped due to instance shutdown"
reaper.reap(other_inst, job_explanation=explanation)
reaper.reap_waiting(other_inst, grace_period=0, job_explanation=explanation)
except Exception:
logger.exception('failed to reap jobs for {}'.format(other_inst.hostname))
try:
@@ -545,6 +563,15 @@ def cluster_node_heartbeat():
else:
logger.exception('Error marking {} as lost'.format(other_inst.hostname))
# Run local reaper
if worker_tasks is not None:
active_task_ids = []
for task_list in worker_tasks.values():
active_task_ids.extend(task_list)
reaper.reap(instance=this_inst, excluded_uuids=active_task_ids)
if max(len(task_list) for task_list in worker_tasks.values()) <= 1:
reaper.reap_waiting(instance=this_inst, excluded_uuids=active_task_ids, ref_time=datetime.fromisoformat(dispatch_time))
@task(queue=get_local_queuename)
def awx_receptor_workunit_reaper():
@@ -592,7 +619,8 @@ def awx_k8s_reaper():
for group in InstanceGroup.objects.filter(is_container_group=True).iterator():
logger.debug("Checking for orphaned k8s pods for {}.".format(group))
pods = PodManager.list_active_jobs(group)
for job in UnifiedJob.objects.filter(pk__in=pods.keys()).exclude(status__in=ACTIVE_STATES):
time_cutoff = now() - timedelta(seconds=settings.K8S_POD_REAPER_GRACE_PERIOD)
for job in UnifiedJob.objects.filter(pk__in=pods.keys(), finished__lte=time_cutoff).exclude(status__in=ACTIVE_STATES):
logger.debug('{} is no longer active, reaping orphaned k8s pod'.format(job.log_format))
try:
pm = PodManager(job)
@@ -660,6 +688,13 @@ def awx_periodic_scheduler():
state.save()
def schedule_manager_success_or_error(instance):
if instance.unifiedjob_blocked_jobs.exists():
ScheduleTaskManager().schedule()
if instance.spawned_by_workflow:
ScheduleWorkflowManager().schedule()
@task(queue=get_local_queuename)
def handle_work_success(task_actual):
try:
@@ -669,8 +704,7 @@ def handle_work_success(task_actual):
return
if not instance:
return
schedule_task_manager()
schedule_manager_success_or_error(instance)
@task(queue=get_local_queuename)
@@ -695,7 +729,7 @@ def handle_work_error(task_id, *args, **kwargs):
first_instance = instance
first_instance_type = each_task['type']
if instance.celery_task_id != task_id and not instance.cancel_flag and not instance.status == 'successful':
if instance.celery_task_id != task_id and not instance.cancel_flag and not instance.status in ('successful', 'failed'):
instance.status = 'failed'
instance.failed = True
if not instance.job_explanation:
@@ -712,27 +746,7 @@ def handle_work_error(task_id, *args, **kwargs):
# what the job complete message handler does then we may want to send a
# completion event for each job here.
if first_instance:
schedule_task_manager()
pass
@task(queue=get_local_queuename)
def handle_success_and_failure_notifications(job_id):
uj = UnifiedJob.objects.get(pk=job_id)
retries = 0
while retries < settings.AWX_NOTIFICATION_JOB_FINISH_MAX_RETRY:
if uj.finished:
uj.send_notification_templates('succeeded' if uj.status == 'successful' else 'failed')
return
else:
# wait a few seconds to avoid a race where the
# events are persisted _before_ the UJ.status
# changes from running -> successful
retries += 1
time.sleep(1)
uj = UnifiedJob.objects.get(pk=job_id)
logger.warning(f"Failed to even try to send notifications for job '{uj}' due to job not being in finished state.")
schedule_manager_success_or_error(first_instance)
@task(queue=get_local_queuename)

View File

@@ -0,0 +1,2 @@
---
- ansible.builtin.import_playbook: foo

View File

@@ -0,0 +1,2 @@
---
- ansible.builtin.include: foo

View File

@@ -2,8 +2,9 @@
"ANSIBLE_JINJA2_NATIVE": "True",
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
"GCE_CREDENTIALS_FILE_PATH": "{{ file_reference }}",
"GOOGLE_APPLICATION_CREDENTIALS": "{{ file_reference }}",
"GCP_AUTH_KIND": "serviceaccount",
"GCP_ENV_TYPE": "tower",
"GCP_PROJECT": "fooo",
"GCP_SERVICE_ACCOUNT_FILE": "{{ file_reference }}"
}
}

View File

@@ -26,6 +26,7 @@ def test_empty():
"workflow_job_template": 0,
"unified_job": 0,
"pending_jobs": 0,
"database_connections": 1,
}

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