Compare commits

...

581 Commits

Author SHA1 Message Date
Alan Rominger
4f1c662691 Merge pull request #11570 from AlanCoding/keycloak_docs
Minor docs tweaks for keycloak setup
2022-01-20 11:52:21 -05:00
Alan Rominger
9abd4e05d0 Minor docs tweaks for keycloak setup 2022-01-20 11:01:32 -05:00
Elijah DeLee
faba64890e Merge pull request #11559 from kdelee/pending_container_group_jobs_take2
Add resource requests to default podspec
2022-01-20 09:54:20 -05:00
Alan Rominger
add54bfd0b Merge pull request #11472 from AlanCoding/process_ident
Pass new ansible-runner parameters to reduce number of artifacts we don't need on file system
2022-01-20 09:48:44 -05:00
John Westcott IV
e63ce9ed08 Api 4XX error msg customization #1236 (#11527)
* Adding API_400_ERROR_LOG_FORMAT setting
* Adding functional tests for API_400_ERROR_LOG_FORMAT
Co-authored-by: nixocio <nixocio@gmail.com>
2022-01-19 11:16:21 -05:00
Kersom
60831cae88 Merge pull request #11539 from nixocio/api_issue_11523
Update ping endpoint to use last_seen
2022-01-19 10:40:02 -05:00
Kersom
97cf46eaa9 Merge pull request #11556 from nixocio/ui_bump_node_npm
Bump node and npm versions inside container
2022-01-19 09:58:30 -05:00
Shane McDonald
381e75b913 Merge pull request #11562 from ansible/avoid_dups_create_preload_data
Avoid duplicated entries when calling create_preload_data
2022-01-18 19:00:43 -05:00
Shane McDonald
7bd516a16c Skip project update 2022-01-18 18:40:58 -05:00
Marcelo Moreira de Mello
3dd01cde89 Avoid duplicated entries when calling create_preload_data 2022-01-18 18:07:26 -05:00
Kersom
495394084d Fix null on workflowjobtemplate (#11522)
Fix null on workflowjobtemplate

See: https://github.com/ansible/awx/issues/11284
2022-01-18 16:54:00 -05:00
Alan Rominger
2609ee5ed0 Delete artifact dir after transmit phase is finished 2022-01-18 14:51:40 -05:00
John Westcott IV
da930ce276 Fixing token documentation (#11550) 2022-01-18 14:21:17 -05:00
Elijah DeLee
987924cbda Add resource requests to default podspec
Extend the timeout, assuming that we want to let the kubernetes scheduler
start containers when it wants to start them. This allows us to make
resource requests knowing that when some jobs queue up waiting for
resources, they will not get reaped in as short of a
timeout.
2022-01-18 13:34:39 -05:00
Alan Rominger
8fac1c18c8 Make task logic use consistent artifact dir location 2022-01-18 13:00:39 -05:00
Alan Rominger
eb64fde885 Pass ident to "process" cmd and disable stdout file
This requires corresponding ansible-runner changes
  which are only available in devel branch
  to do this, requirements are changed
  to install ansible-runner devel as it did before

Revert "Use ansible-runner 2.1.1 build"

This reverts commit f0ede01017.

Add back in change from updater.sh that we want to keep
2022-01-18 13:00:39 -05:00
nixocio
b1e9537499 Bump node and npm versions inside container
Bump node and npm versions inside container

Prepating to bump react scripts to 5.0.

See: https://github.com/ansible/awx/issues/11543
2022-01-17 20:33:47 -05:00
Alan Rominger
696c0b0055 Merge pull request #11503 from AlanCoding/no_version
Remove unused ansible version method
2022-01-14 22:15:15 -05:00
Jeff Bradberry
6e030fd62f Merge pull request #11546 from jbradberry/remove-instance-activecount
Remove the Instance.objects.active_count() method
2022-01-14 16:46:01 -05:00
Jeff Bradberry
bb14a95076 Remove the Instance.objects.active_count() method
Literally nothing uses it.  The similar Host.objects.active_count()
method seems to be what is actually important for licensing.
2022-01-14 16:21:41 -05:00
Alan Rominger
9664aed1f2 Remove unused ansible version method 2022-01-14 14:55:35 -05:00
Amol Gautam
6dda5f477e Merge pull request #11544 from AlanCoding/another_rule
Respect linter rule F811 about trivial re-definition
2022-01-14 14:05:41 -05:00
Alan Rominger
72cd73ca71 Update to cover stuff from tasks.py changes 2022-01-14 13:42:24 -05:00
Alan Rominger
02e18cf919 Fix more F811 linter violations 2022-01-14 13:23:05 -05:00
Alan Rominger
82671680e3 Respect linter rule F811 for trivial re-definition 2022-01-14 13:23:04 -05:00
Amol Gautam
bff49f2a5f Merge pull request #11528 from amolgautam25/tasks-refactor-1
Refactored 'tasks.py' file  into a package
2022-01-14 12:16:32 -05:00
Marcelo Moreira de Mello
59d582ce83 Merge pull request #11530 from ansible/dont_expose_k8s_api_token_by_default
Don't expose serviceAccount token on default pod spec
2022-01-14 12:04:14 -05:00
Amol Gautam
a4a3ba65d7 Refactored tasks.py to a package
--- Added 3 new sub-package : awx.main.tasks.system , awx.main.tasks.jobs , awx.main.tasks.receptor
--- Modified the functional tests and unit tests accordingly
2022-01-14 11:55:41 -05:00
Kersom
11f4b64229 Modify how manual subform is displayed for projects (#11509)
Modify how manual subform is displayed for projects - Do not rely on
label that could be translated, rely on the value.

See: https://github.com/ansible/awx/issues/11505
2022-01-14 11:19:10 -05:00
Jeff Bradberry
b76029fac3 Merge pull request #11538 from jbradberry/fix-exact-removals-for-register-peers
Fix the logic for register_peers --exact
2022-01-14 09:42:51 -05:00
nixocio
3d45f31536 Update ping endpoint to use last_seen
Update ping endpoint to use last_seen, instead of `modified` on
instances `heartbeat`.

See: https://github.com/ansible/awx/issues/11523
2022-01-13 16:46:40 -05:00
Jeff Bradberry
ade00c70e5 Merge pull request #11537 from jbradberry/enhancements-for-meshviz-endpoint
Enhancements for meshviz endpoint
2022-01-13 16:42:21 -05:00
Jeff Bradberry
82dca5336d Fix the logic for register_peers --exact
- correctly calculate the extraneous peers
- allow --exact to take an empty set of arguments, to remove all peers
2022-01-13 15:41:45 -05:00
Jeff Bradberry
8c33d0ecbd Add the mesh_visualizer resource to awxkit 2022-01-13 15:01:54 -05:00
Jeff Bradberry
dea5fd1a9d Fix a problem with IsSystemAdminOrAuditor for anonymous users
It was raising an error, but should really show the message about not
being authenticated.
2022-01-13 14:44:50 -05:00
Jeff Bradberry
6a131f70f0 Require System Admin or Auditor permissions to access the mesh visualizer 2022-01-13 14:13:17 -05:00
Alex Corey
d33a0d5dde Merge pull request #11454 from AlexSCorey/ReceptorEndPoints
Creates end point and beginning of serializer for receptor mesh
2022-01-13 11:51:34 -05:00
Jeff Bradberry
7e6cb7ecc9 Merge pull request #11533 from jbradberry/fix-register-peers-exact-typo
Fix the loop variable name for the register_peers --exact flag
2022-01-13 11:28:15 -05:00
Jeff Bradberry
807c58dc36 Fix the loop variable name for the register_peers --exact flag 2022-01-13 11:05:26 -05:00
Marcelo Moreira de Mello
1517f2d910 Don't expose serviceAccount token on default pod spec 2022-01-12 23:47:24 -05:00
Alan Rominger
b0c59ee330 Merge pull request #11375 from AlanCoding/missing_image_error_devel
Fail with specific error message if protected image is not available
2022-01-12 11:05:17 -05:00
Jeff Bradberry
1ff52bab56 Merge pull request #11520 from jbradberry/fix-register-peers
In register_peers, only check non-empty flags for the 1-cycle check
2022-01-11 16:52:53 -05:00
Jeff Bradberry
7a9fca7f77 In register_peers, only check non-empty flags for the 1-cycle check 2022-01-11 16:16:33 -05:00
Alex Corey
dea53a0dba Creates end point and serializer for receptor mesh 2022-01-11 10:57:57 -05:00
Jeff Bradberry
db999b82ed Merge pull request #11431 from jbradberry/receptor-mesh-models
Modify Instance and introduce InstanceLink
2022-01-11 10:55:54 -05:00
John Westcott IV
c92468062d SAML user attribute flags issue #5303 (PR #11430)
* Adding SAML option in SAML configuration to specify system auditor and system superusers by role or attribute
* Adding keycloak container and documentation on how to start keycloak alongside AWX (including configuration of both)
2022-01-10 16:52:44 -05:00
Seth Foster
4de0f09c85 Merge pull request #11515 from fosterseth/revert_debug_level
Revert "Remove unnecessary DEBUG logger level settings (#11441)"
2022-01-10 16:38:33 -05:00
Jeff Bradberry
9c9c1b4d3b register_peers will now raise errors if you attempt to reverse or loop 2022-01-10 15:48:17 -05:00
Jeff Bradberry
5ffe91f069 Add a new --exact parameter to register_peers 2022-01-10 15:12:04 -05:00
Jeff Bradberry
63867518ee Add a new parameter --disconnect to register_peers
To allow links between Receptor nodes to be removed from the database.
2022-01-10 14:15:58 -05:00
Sarah Akus
53ff99e391 Merge pull request #11513 from marshmalien/10241-test-locator
Add test locators to OUIA-compliant components
2022-01-10 13:10:08 -05:00
Shane McDonald
c035c12c0a Merge pull request #11380 from sean-m-sullivan/new_name
add new name to multiple modules
2022-01-11 01:42:55 +08:00
Shane McDonald
6e39a02e99 Merge pull request #11504 from sean-m-sullivan/devel
add better error and documentation on labels
2022-01-11 01:42:13 +08:00
Seth Foster
956638e564 Revert "Remove unnecessary DEBUG logger level settings (#11441)"
This reverts commit 8126f734e3.
2022-01-10 11:46:19 -05:00
Jeff Bradberry
37907ad348 Register the hop & execution nodes and all node links 2022-01-10 11:37:19 -05:00
Jeff Bradberry
386aa898ec Remove the make init target
we want to fold that in to bootstrap_environment.sh.
2022-01-10 11:37:19 -05:00
Jeff Bradberry
f1c5da7026 Remove the auto-discovery feature 2022-01-10 11:37:19 -05:00
Jeff Bradberry
fc2a5224ef Add error messages to the new register_peers command 2022-01-10 11:37:19 -05:00
Jeff Bradberry
ce5aefd3d8 Capture hop nodes and links in the automatic discovery machinery
Also, make sure that the control service is turned on in the dev
environment's hop node, so that it shows up in the Advertisements
list.
2022-01-10 11:37:13 -05:00
Marliana Lara
b2124dffb5 Add test locators to OUIA-compliant components 2022-01-07 14:39:18 -05:00
Christian Adams
25eaace4be Merge pull request #11508 from tchellomello/awx-config-watcher-dies-ocp
Disable awx-config-watcher for k8s images
2022-01-07 10:01:19 -05:00
sean-m-ssullivan
bb8efbcc82 add new name to multiple modules 2022-01-05 22:33:51 -05:00
sean-m-sullivan
e0bd5ad041 add better error and documentation on labels 2022-01-05 20:09:02 -05:00
Marcelo Moreira de Mello
69ec49d0e9 Disable awx-config-watcher on OCP 2022-01-05 17:02:14 -05:00
Alan Rominger
8126f734e3 Remove unnecessary DEBUG logger level settings (#11441)
* Remove unnecessary DEBUG logger level settings
2022-01-05 14:44:57 -05:00
nixocio
f2aaa6778c Add warning message for K8S deployment
Add warning message for K8S deployment
2022-01-05 11:32:59 -05:00
Sarah Akus
4fd5b01a83 Merge pull request #11324 from keithjgrant/10655-duplicate-api-requests
Reduce duplicate fetches after saving inventory group
2022-01-04 11:42:37 -05:00
Jeff Bradberry
1747a844fc Merge pull request #11485 from jbradberry/fix-broken-events-analytics
Fix a problem with the events_table analytics collectors
2022-01-04 11:30:26 -05:00
Kersom
afc210a70d Merge pull request #11489 from nixocio/ui_issue_11452
Fix relaunch of jobs
2022-01-04 08:34:50 -05:00
Keith J. Grant
f63003f982 don't navigate to inventory group details on edit cancel 2021-12-21 13:22:59 -08:00
Keith J. Grant
e89037dd77 reduce duplicate fetches after saving inventory group 2021-12-21 13:22:59 -08:00
nixocio
ab6e650e9c Fix relaunch of jobs
Events were passed to `handleRelaunch` and those events structure were
not parseable to JSON - breaking the relaunch of jobs. React 17 changes
made this bug visible.

Also, remove withRouter from LaunchButton.

See: https://github.com/ansible/awx/issues/11452
2021-12-21 14:39:34 -05:00
Jeff Bradberry
2ed246cb61 Fix a problem with the events_table analytics collectors
The switch to using jsonb objects instead of json broke the use of
json_to_record in the raw sql in the _events_table function.
2021-12-20 14:03:24 -05:00
Jeff Bradberry
4449555abe Add a new register_peers management command
and alter provision_instance to accept hop nodes.
2021-12-20 09:56:48 -05:00
Jeff Bradberry
f340f491dc Control the visibility and use of hop node Instances
- the list, detail, and health check API views should not include them
- the Instance-InstanceGroup association views should not allow them
  to be changed
- the ping view excludes them
- list_instances management command excludes them
- Instance.set_capacity_value sets hop nodes to 0 capacity
- TaskManager will exclude them from the nodes available for job execution
- TaskManager.reap_jobs_from_orphaned_instances will consider hop nodes
  to be an orphaned instance
- The apply_cluster_membership_policies task will not manipulate hop nodes
- get_broadcast_hosts will ignore hop nodes
- active_count also will ignore hop nodes
2021-12-17 14:30:28 -05:00
Jeff Bradberry
c8f1e714e1 Capture hop nodes and the peer links between nodes 2021-12-17 14:30:18 -05:00
Sarah Akus
ddc428532f Merge pull request #11470 from rebeccahhh/devel
Jobs page filter status with OR operator
2021-12-16 16:45:47 -05:00
Jeff Bradberry
3414cae677 Merge pull request #11471 from jbradberry/failure-notification-fallback-explanation
Only update the job_explanation on error if there wasn't already one
2021-12-16 11:10:38 -05:00
Wambugu “Innocent” Kironji
9d6972c6ce Merge pull request #11459 from marshmalien/5456-insights-system-settings
Update label and display of "Last gathered entries..." setting
2021-12-15 16:58:18 -05:00
Marliana Lara
0566a0f1d6 Update label and display of "Last gathered entries..." setting 2021-12-15 15:59:43 -05:00
Jeff Bradberry
de0561dcc2 Only update the job_explanation on error if there wasn't already one 2021-12-15 15:24:04 -05:00
Rebeccah
a9f4f53f92 change logical ANDs into logical ORs for filtering based on status in the JobsList 2021-12-15 15:15:33 -05:00
Elijah DeLee
5fdfd4114a Merge pull request #11395 from kdelee/override_default_container_group_pod_spec
Allow setting default execution group pod spec
2021-12-15 13:57:47 -05:00
Jeff Bradberry
b195f9da44 Merge pull request #11384 from jbradberry/failure-notification-on-error
Make sure to fire off failure notifications on error
2021-12-15 13:47:10 -05:00
Tiago Góes
1205d71f4b Merge pull request #11466 from tiagodread/restore-locator-2
Restore locator removed
2021-12-15 11:10:26 -03:00
Tiago
3f762a6476 restore locator removed 2021-12-15 10:55:02 -03:00
Tiago Góes
4aa403c122 Merge pull request #11465 from tiagodread/restore-locator
Restore locator removed
2021-12-14 18:57:22 -03:00
Tiago
a13070a8da restore locator removed 2021-12-14 18:39:10 -03:00
Wambugu “Innocent” Kironji
b63b171653 Merge pull request #11447 from nixocio/ui_issue_7561
Add email as default search key user lists
2021-12-14 16:29:10 -05:00
Alan Rominger
7219f8fed8 Merge pull request #11462 from AlanCoding/forgot_this
Add the cancel_callback to system job interface
2021-12-14 14:17:47 -05:00
Alan Rominger
b6a5f834d6 Merge pull request #11408 from amolgautam25/receptor_tech_debt
Removing time.sleep(3)
2021-12-14 11:54:49 -05:00
Alan Rominger
99b9d53bbb Add the cancel_callback to system job interface 2021-12-14 10:50:39 -05:00
Alex Corey
edca19a697 Merge pull request #11402 from AlexSCorey/upgradePF
Updates patternfly dependencies
2021-12-13 11:02:01 -05:00
Jake McDermott
c13d721062 Merge pull request #11435 from jakemcdermott/fix-vaulted-ee-cred
Handle exception for credential input checks in calling function
2021-12-13 10:10:23 -05:00
Kersom
d2f316c484 Merge pull request #11443 from nixocio/ui_issue_11442
Fix extra requests when creating WorkFlowJobTemplate
2021-12-13 09:12:27 -05:00
nixocio
70e832d4db Fix extra requests when creating WorkFlowJobTemplate
Fix extra requests when creating WorkFlowJobTemplate

See: https://github.com/ansible/awx/issues/11442
2021-12-13 08:19:24 -05:00
Alan Rominger
21895bd09b Merge pull request #11448 from AlanCoding/revert_again
Revert "cancel job if receptor no longer knows about the work item"
2021-12-10 16:35:12 -05:00
Alan Rominger
411ef5f9e8 Revert "cancel job if receptor no longer knows about the work item"
This reverts commit 2a11bb4f3b.
2021-12-10 16:18:44 -05:00
nixocio
f6282b9a09 Add email as default search key user lists
Add email as default search key user lists

See: https://github.com/ansible/awx/issues/7561
2021-12-10 16:06:38 -05:00
Elijah DeLee
e10030b73d Allow setting default execution group pod spec
This will allow us to control the default container group created via settings, meaning
we could set this in the operator and the default container group would get created with it applied.

We need this for https://github.com/ansible/awx-operator/issues/242

Deepmerge the default podspec and the override

With out this, providing the `spec` for the podspec would override everything
contained, which ends up including the container used, which is not desired

Also, use the same deepmerge function def, as the code seems to be copypasted from
the utils
2021-12-10 15:02:45 -05:00
Jeff Needle
cdf14158b4 Merge pull request #11436 from AlexSCorey/sync
Pulling in upstream changes
2021-12-10 14:48:59 -05:00
Alex Corey
f310e672b0 Merge pull request #11247 from AlexSCorey/11227-fix
Removes disassociate button on details view and fine tunes disassociate button on list view
2021-12-10 10:30:30 -05:00
Keith Grant
675d0d28d2 Job Output expand/collapse take 2 (#11312) 2021-12-09 14:08:31 -05:00
Alex Corey
4c2fd056ef updated patternfly 2021-12-09 12:09:58 -05:00
Sarah Akus
a259e48377 Merge pull request #11414 from AlexSCorey/upgradeReact
Upgrade react
2021-12-09 09:53:35 -05:00
ansible-translation-bot
095c586172 UI translation strings for release_4.1 branch
* Correct syntax errors & add back lost last line for messages.po
  * Manually sort through es & nl translated strings
  * Mnaually sort through french strings and correct syntax errors

Signed-off-by: Christian M. Adams <chadams@redhat.com>
2021-12-08 15:57:08 -05:00
Jeff Bradberry
c9c198b54b Fix the problems with the api-schema tests against Tower
- add the appropriate release branch to the branches list
- add a fallback to the `docker pull` command
2021-12-08 15:57:08 -05:00
Jim Ladd
2a11bb4f3b cancel job if receptor no longer knows about the work item
lint
2021-12-08 15:57:02 -05:00
Shane McDonald
35bac50962 Ensure docker pull commands fail gracefully 2021-12-08 15:51:14 -05:00
jakemcdermott
366d2c1d97 Handle exception for credential input checks in calling function 2021-12-08 12:09:20 -05:00
Jake McDermott
9a930cbd95 Merge pull request #10935 from jakemcdermott/remove-sleep
Remove sleep from tests
2021-12-08 11:14:36 -05:00
Jake McDermott
03277513a9 Remove sleep from tests 2021-12-08 10:55:30 -05:00
Alan Rominger
1b0fca8026 Merge pull request #11386 from AlanCoding/logs_on_the_fire
Remove dev-only log filters and downgrade periodic logs
2021-12-07 16:13:45 -05:00
Christian Adams
c9cf5b78c5 Merge pull request #11428 from rooftopcellist/fix-k8s-image-build
Make awx-python script available in k8s app images
2021-12-07 14:36:31 -05:00
Alan Rominger
d6679a1e9b Respect dynamic log setting for console, downgrade exit log 2021-12-07 14:35:03 -05:00
Alan Rominger
b721a4b361 Remove dev-only log filters and downgrade periodic logs 2021-12-07 14:35:02 -05:00
Christian M. Adams
88bbd43314 Make awx-python script available in k8s app images 2021-12-07 13:48:32 -05:00
Tiago Góes
fb1c97cdc1 Merge pull request #11311 from nixocio/ui_no_more_classes
Convert last class components to functional components
2021-12-07 14:57:47 -03:00
Kersom
f5ae8a0a4c Merge pull request #11377 from nixocio/ui_sonic_tests
Update how ui tests are invoked on CI
2021-12-07 09:42:34 -05:00
nixocio
1994eaa406 Convert last class components to functional components
Convert last class components to functional components
2021-12-07 09:19:49 -05:00
nixocio
510b40a776 Update how ui tests are invoked on CI
Update how ui tests are invoked on CI as an attempt to speed up test
run.
2021-12-07 09:18:32 -05:00
Alex Corey
f37b070965 Upgrades React 2021-12-06 14:36:08 -05:00
Alex Corey
41385261f3 Resolves disassociate button for instances 2021-12-06 11:32:12 -05:00
Alan Rominger
19b4849345 Merge pull request #11394 from notok/cfg_from_template_branch
Load ansible.cfg from the branch specified on job template
2021-12-06 11:09:36 -05:00
notok
76283bd299 Load ansible.cfg from the branch specified on job template
Load ansible.cfg from the branch specified on job template (i.e. the same branch that the playbook exists), not from the branch set in the "project".

Signed-off-by: notok <noto.kazufumi@gmail.com>
2021-12-03 20:36:07 +09:00
Amol Gautam
2e4cda74c8 Removing time.sleep(3) 2021-12-02 15:41:46 -05:00
Alan Rominger
5512b71e16 Merge pull request #11412 from AlanCoding/cookie_revert
Revert "Set SESSION_COOKIE_NAME by default"
2021-12-02 11:00:56 -05:00
Alan Rominger
97b60c43b7 Merge pull request #11385 from AlanCoding/my_cluster_host
Do not overwrite file-based CLUSTER_HOST_ID written by installer
2021-12-02 10:53:59 -05:00
Alan Rominger
35b62f8526 Revert "Set SESSION_COOKIE_NAME by default"
This reverts commit 59c6f35b0b.
2021-12-01 17:51:47 -05:00
Kersom
a15a3f005c Merge pull request #11278 from nixocio/ui_bump
Bump Browserslist version
2021-12-01 09:26:19 -05:00
Alan Rominger
776c4a988a Do not overwrite file-based CLUSTER_HOST_ID written by installer 2021-11-30 20:15:10 -05:00
Jeff Bradberry
c419969253 Make sure to fire off failure notifications on error
where the error is unrelated to Ansible, thus is not caught by the
usual methods.
2021-11-23 13:25:08 -05:00
Jake McDermott
ba324c73ce Merge pull request #11378 from ansible/update-dev-env-readme
Update example command for running test container
2021-11-19 16:09:37 -05:00
Jake McDermott
4a5dc78331 Update example command for running test container 2021-11-19 15:44:51 -05:00
Kersom
55dc9dfb54 Merge pull request #11355 from nixocio/ui_issue_11352
Linkify instance/container groups job template details
2021-11-19 14:59:31 -05:00
nixocio
23a8191bb5 Bump Browserslist version
Bump Browserslist version to remove warning.

See: https://github.com/browserslist/browserslist#browsers-data-updating
2021-11-19 14:53:01 -05:00
nixocio
c665caaf35 Linkify instance/container groups job template
Linkify instance/container groups job template

See: https://github.com/ansible/awx/issues/11352
2021-11-19 14:23:11 -05:00
Alan Rominger
099efb883d Allow customizing the receptor image in the development environment (#11374)
* Allow for customizing the receptor image

* Hook in receptor image to docker-compose template

* Fix missing -e to pass into Dockerfile playbook

* Add some docs
2021-11-19 14:00:23 -05:00
Sarah Akus
44237426df Merge pull request #11353 from nixocio/ui_node_delete
Identify node to be deleted on workflow
2021-11-19 12:32:27 -05:00
Alan Rominger
eeefd19ad3 Fail with specific error message if protected image is not available locally 2021-11-19 11:52:54 -05:00
nixocio
47ae6e7a5a Identify node to be deleted on workflow
Identify node to be deleted on workflow. If there is an alias show the
alias if no alias is available show the node name.

See: https://github.com/ansible/awx/issues/11351
2021-11-19 10:55:19 -05:00
Shane McDonald
03ed6e9755 Merge pull request #11371 from shanemcd/document-release-process
Document release process
2021-11-19 18:43:53 +08:00
Shane McDonald
8d4e7f0a82 Document release process 2021-11-19 08:28:48 +00:00
Shane McDonald
7fdf491c05 Merge pull request #11369 from shanemcd/lets-automate-everything
Automate the rest of our release process
2021-11-19 11:37:58 +08:00
Shane McDonald
ef1563283e An automated stage / promotion release process 2021-11-19 02:22:45 +00:00
Shane McDonald
a206d79851 Merge pull request #11368 from shanemcd/downstream-changes
A few more downstream fixes
2021-11-19 09:46:21 +08:00
Satoe Imaishi
42c9c0a06b Use receptor 1.1.1 build 2021-11-19 01:11:35 +00:00
Satoe Imaishi
f0ede01017 Use ansible-runner 2.1.1 build 2021-11-19 01:11:19 +00:00
Alan Rominger
d67007f777 Move only_transmit_kwargs calculation out of thread 2021-11-19 01:11:18 +00:00
nixocio
83d81e3788 Upgrade has-ansi 2021-11-19 01:10:36 +00:00
Shane McDonald
e789e16289 Merge pull request #11348 from pabelanger/temp/sessionname
Set SESSION_COOKIE_NAME by default
2021-11-19 08:33:07 +08:00
Bianca Henderson
61c9683aa6 Merge pull request #11269 from AlexSCorey/1741-SlackNotifications
Users can send slack notification to a thread
2021-11-18 14:28:28 -05:00
Sarah Akus
ee9d1356b2 Merge pull request #11354 from nixocio/ui_issue_11350
Update search keys
2021-11-17 14:56:46 -05:00
Alex Corey
f92a49fda9 Adds ability to send slack notification to a thread, updates tooltip in ui, and adds test button to notification details view 2021-11-17 14:04:32 -05:00
nixocio
3dc6a055ac Update search keys
Update search keys.

See: https://github.com/ansible/awx/issues/11350
2021-11-16 15:32:50 -05:00
Kersom
229f0d97f9 Merge pull request #11307 from jakemcdermott/default-template-search-labels
Add labels to default template search
2021-11-16 15:14:55 -05:00
Christian Adams
7cc530f950 Merge pull request #11145 from aperigault/devel
fix french typos
2021-11-16 11:23:18 -05:00
aperigault
2ef840ce12 Fix encrypted translation 2021-11-16 16:27:27 +01:00
Antony Perigault
a372d8d1d5 fix french typos 2021-11-16 16:27:27 +01:00
Shane McDonald
be13a11dd5 Merge pull request #11344 from Akasurde/typo
Misc typo fix
2021-11-16 16:52:30 +08:00
Paul Belanger
59c6f35b0b Set SESSION_COOKIE_NAME by default
Make sure to use a different session cookie name then the default, to
avoid overlapping cookies with other django apps that might be running.

Signed-off-by: Paul Belanger <pabelanger@redhat.com>
2021-11-15 12:59:07 -05:00
Abhijeet Kasurde
37e45c5e7c Misc typo fix
Changed 'controler' to 'controller'

Signed-off-by: Abhijeet Kasurde <akasurde@redhat.com>
2021-11-15 16:24:21 +05:30
Shane McDonald
aec7ac6ebd Merge pull request #11341 from shanemcd/fix-image-builds
Fix official image builds
2021-11-13 14:31:26 +08:00
Shane McDonald
f6e63d0917 Fix official image builds
I broke everything in https://github.com/ansible/awx/pull/11242.

These changes were necessary in order to run `awx-manage collectstatic` without a running database.
2021-11-13 06:07:37 +00:00
Rebeccah Hunter
0ae67edaba Merge pull request #11267 from ziegenberg/add-tests-for-webhook-notifications
Add unit tests for webhook notifications
2021-11-11 09:55:38 -05:00
Shane McDonald
481f6435ee Merge pull request #11327 from shanemcd/downstream-changes
Pull in downstream changes
2021-11-11 11:09:22 +08:00
chris meyers
d0c5c3d3cf add work_unit_id to job lifecycle 2021-11-10 08:50:16 +08:00
chris meyers
9f8250bd47 add events to job lifecycle
* Note in the job lifecycle when the controller_node and execution_node
  are chosen. This event occurs most commonly in the task manager with a
  couple of exceptions that happen when we dynamically create dependenct
  jobs on the fly in tasks.py
2021-11-10 08:50:16 +08:00
Alan Rominger
3a3fffb2dd Fixed error dropped on floor - save receptor detail when it applies 2021-11-10 08:50:16 +08:00
nixocio
4cfa4eaf8e Update validators for Misc Auth Edit
* Update SharedFields to use number validator instead of integer
* Use number validation for SESSIONS_PER_USER

See: https://github.com/ansible/tower/issues/5396
2021-11-10 08:50:16 +08:00
Kersom
abb1125a2c Display host name for Associate Modal (#5407)
Display host name for Associate Modal

See: https://github.com/ansible/awx/issues/11256
2021-11-10 08:50:16 +08:00
Alan Rominger
a2acbe9fe6 Fix incorrect (changed: True) frequent in OCP task logs 2021-11-10 08:50:16 +08:00
Alex Corey
cab8c690d2 Adds instances to aactivty stream 2021-11-10 08:50:16 +08:00
Alan Rominger
0d1f8a06ce Revert default EE authfile support for inventory_import 2021-11-10 08:50:15 +08:00
Alan Rominger
d42fe921db Re-order authfile option to make inventory import command work 2021-11-10 08:50:15 +08:00
Kersom
db7fb81855 Fix login redirect (#5386)
Allows the user to visit login page when the login redirect url is set.

Also, redirects to login page once logging out and there is session from
a SAML available.

See: https://github.com/ansible/awx/issues/11012
2021-11-10 08:50:15 +08:00
Jeff Bradberry
d3c695b853 Clean up some scar tissue left behind
by the initial use of the black code formatter.
2021-11-10 08:50:15 +08:00
Jeff Bradberry
010c3ab0b8 Fix a typo in inventory_import
ExecutionEnvironment.credential got shortened to .cred.
2021-11-10 08:50:15 +08:00
Bianca Henderson
58cdbca5cf Update error message to be more accurate 2021-11-10 08:50:15 +08:00
Bianca Henderson
8275082896 Update error messages for when exceptions are caught 2021-11-10 08:50:14 +08:00
Bianca Henderson
d79da1ef9f Catch exceptions that might pop up when releasing work units 2021-11-10 08:50:14 +08:00
Jeff Bradberry
a9636426b8 Make the inventory_import command respect the default EE and credential 2021-11-10 08:50:14 +08:00
Alan Rominger
329caad681 In admin reaper skip work units w/o params 2021-11-10 08:50:14 +08:00
Alan Rominger
ecb84e090c Revert "Merge pull request #5354 from ansible/jobs_killed_via_receptor_should_get_reaped"
This reverts commit 8736858d80, reversing
changes made to 84e77c9db9.
2021-11-10 08:50:14 +08:00
nixocio
8e9fc14b0e Fix SAML variables default values
Fix SAML variables default values

See: https://github.com/ansible/tower/issues/5372
2021-11-10 08:50:14 +08:00
Jim Ladd
0f77ca605d add unit tests 2021-11-10 08:50:14 +08:00
Jim Ladd
231fcc8178 drop lines picked up during merge resolution 2021-11-10 08:50:13 +08:00
Alan Rominger
2839091b22 Avoid extra check if we have job_explanation 2021-11-10 08:50:13 +08:00
Alan Rominger
47e67481b3 Avoid reaping tentative jobs 2021-11-10 08:50:13 +08:00
Alan Rominger
55059b015f Avoid resultsock shutdown before reading from it 2021-11-10 08:50:13 +08:00
Alan Rominger
eb6c58682d Alternative for reaping lost jobs, in work unit reaper 2021-11-10 08:50:13 +08:00
Jim Ladd
26055de772 cancel job if receptor no longer knows about the work item 2021-11-10 08:50:13 +08:00
Jim Ladd
ebb4581595 update exception log message to be descriptive
.. instead of surfacing exception
2021-11-10 08:50:12 +08:00
Jim Ladd
d1fecc11c9 when releasing receptor work, do so in try/except 2021-11-10 08:50:12 +08:00
Jeff Bradberry
056247a34a Adjust Instance-InstanceGroup tests to show that the ActivityStream is captured 2021-11-10 08:50:12 +08:00
Jeff Bradberry
7010015e8a Change the ActivityStream registration for InstanceGroups
to include the m2m fields.  Also to avoid spamminess, disable the
activity stream on the apply_cluster_membership_policies task.
2021-11-10 08:50:12 +08:00
Jeff Bradberry
62d50d27be Update a couple of the existing tests 2021-11-10 08:50:12 +08:00
Jeff Bradberry
1e5231d68b Enable ActivityStream capture for Instances 2021-11-10 08:50:12 +08:00
Seth Foster
e04efad3c0 tools_receptor_1 should use whatever awx_devel tag that tools_awx_1 is using 2021-11-10 08:50:11 +08:00
Alan Rominger
e54db3ce50 Gracefully handle receptorctl RuntimeError in health check 2021-11-10 08:50:11 +08:00
Alan Rominger
77076dbd67 Reduce the number of triggers for execution node health checks 2021-11-10 08:50:11 +08:00
Alan Rominger
6f20a798ab Allow testing a single hybrid instance like the good old days 2021-11-10 08:50:11 +08:00
Alex Corey
0d3a22bbc3 Fixes erroneous validation 2021-11-10 08:50:11 +08:00
Alan Rominger
f34c96ecf5 Error handling when node is missing from mesh for jobs and checks 2021-11-10 08:50:11 +08:00
nixocio
206c85778e Do not show control instances as option to be associated
Do not show control instances as option to be associated to user defined
instance groups.

See: https://github.com/ansible/tower/issues/5339
2021-11-10 08:50:11 +08:00
Marcelo Moreira de Mello
d6b4b9f973 Added node_type on awx-manage list_instances commmand
(cherry picked from commit 683145e3eaa8b13da59bc51e57dff98f25d3554d)
2021-11-10 08:50:10 +08:00
chris meyers
3065e29deb avoid work_results and work release race
* Unsure exactly why this happens but there seems to be a race condition
  related to the time window between Receptor work_results and work
  release. This sleep extends that window and hopefully avoids the race
  condition.
2021-11-10 08:50:10 +08:00
Bianca Henderson
481047bed8 Change log level from 'warning' to 'exception' 2021-11-10 08:50:10 +08:00
Bianca Henderson
f72292cce2 Move error handling into try/catch block 2021-11-10 08:50:10 +08:00
Alan Rominger
7b35902d33 Respect settings to keep files and work units
Add new logic to cleanup orphaned work units
  from administrative tasks

Remove noisy log which is often irrelevant
  about running-cleanup-on-execution-nodes
  we already have other logs for this
2021-11-10 08:50:10 +08:00
Shane McDonald
1660900914 Dont fail CI when pre-built images arent available
CI will build the image from scratch if the pre-build image is not available
2021-11-10 08:50:08 +08:00
kialam
a7be25ce8b Merge pull request #11282 from kialam/upgrade-d3-to-v7
Upgrade d3 to v7.
2021-11-04 14:06:23 -07:00
Tiago Góes
54b5ba08b8 Merge pull request #11259 from tiagodread/update-e2e-script
Fix e2e tests workflow
2021-11-04 13:06:38 -03:00
jakemcdermott
0fb8d48074 Add labels to default template search 2021-11-04 10:35:24 -04:00
Rebeccah Hunter
b5fac4157d Merge pull request #11281 from ziegenberg/update-docs-to-include-openssl-as-requirement
add OpenSSL to the list of prerequisites
2021-11-01 13:02:52 -04:00
Bianca Henderson
9e61949f9f Merge pull request #11263 from ziegenberg/fix-documentation-link-to-debugging
fix link to debugging documentation
2021-11-01 11:53:01 -04:00
Daniel Ziegenberg
6c5640798f fix link to debugging documentation
Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2021-10-30 18:45:46 +02:00
Bianca Henderson
03222197a3 Merge pull request #11270 from ziegenberg/update-slack-sdk
Update dependency slackclient to slack_sdk
2021-10-29 17:33:29 -04:00
Alan Rominger
12f417d0a3 Merge pull request #11286 from StarryInternet/enpaul-multiuse-mesh
Skip additional instance checks on unrecognized hosts
2021-10-29 15:09:33 -04:00
Ethan Paul
c77aaece1d Skip additional instance checks on unrecognized hosts
Skip checking the health of a mesh instance when the instance is not registered
with the application. This prevents encountering an 'UnbouncLocalError' when
running the application attached to a multi-use Receptor mesh network

Signed-off-by: Ethan Paul <24588726+enpaul@users.noreply.github.com>
2021-10-29 14:06:36 -04:00
Shane McDonald
25140c9072 Merge pull request #11288 from bhavenst/devel
Fix dev build (docker-compose) problems
2021-10-28 12:54:13 -04:00
Bryan Havenstein
3a636c29ab Fix dev build (docker-compose) problems
Prevent deletion of nginx user by entrypoint.sh
 - Fixes: https://github.com/ansible/awx/issues/9552

Enable fuse-overlayfs in all images - native overlay not supported until kernel 5.13+
 - Fixes: https://github.com/ansible/awx/issues/10099

Refs:
https://www.redhat.com/sysadmin/podman-rootless-overlay
https://www.redhat.com/en/blog/working-container-storage-library-and-tools-red-hat-enterprise-linux
2021-10-27 15:55:57 -06:00
Kia Lam
a11d5ccd37 Add missing UI license file. 2021-10-27 10:58:31 -07:00
Daniel Ziegenberg
f6e7937f74 Add unit tests for webhook notifications
Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2021-10-27 17:33:37 +02:00
Rebeccah Hunter
e447b667e5 Merge pull request #11246 from ziegenberg/fix-http-headers-for-rocketchat-notifications
Use the AWX HTTP client headers for rocketchat notifications
2021-10-27 10:20:58 -04:00
Kia Lam
24c635e9bc Fix unit tests. 2021-10-26 14:48:58 -07:00
Kia Lam
2ad4dcd741 Upgrade d3 to v7. 2021-10-26 12:07:15 -07:00
Daniel Ziegenberg
f5cd9e0799 add OpenSSL to the list of prerequisites
For running `make docker-compose` a working version of openssl is
required for successfully generating Private RSA key for signing work.

Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2021-10-26 17:14:00 +02:00
Daniel Ziegenberg
e7064868b4 updates the implementation of the slack backend for notifications
Use the slack_sdk instead of the deprecated slackclient. Because according to the official documentation:
>  The slackclient PyPI project is in maintenance mode now and slack-sdk project is the successor.
With this commit one UPGRADE BLOCKER from requirements/requirements.in is removed. Als the license for slack_sdk
is updated and unit tests for slack notifications backend are added.

Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2021-10-26 16:41:10 +02:00
Daniel Ziegenberg
65cbbf15c9 Use the AWX HTTP client headers for rocketchat notifications
Signed-off-by: Daniel Ziegenberg <daniel@ziegenberg.at>
2021-10-20 13:14:30 +02:00
Tiago
a325509e1e Fix e2e check 2021-10-19 15:23:43 -03:00
Jake McDermott
69ae731898 Merge pull request #11258 from ansible/jakemcdermott-include-jsconfig
Add jsconfig to frontend container
2021-10-19 14:02:54 -04:00
Jake McDermott
3452dee1b0 Add jsconfig to frontend container
The eslint and jsconfig files are needed to start the dev server.

Without the jsconfig, the ui development server can't resolve src 
modules and will fail to start.
2021-10-19 12:05:15 -04:00
Shane McDonald
64b337e3c6 Dont re-run CI after merging PRs into devel 2021-10-19 11:24:28 -04:00
Bianca Henderson
5df9655fe3 Merge pull request #11252 from beeankha/update_version_makefile_target
Update/Add Targets that Acquire AWX Version
2021-10-19 10:59:48 -04:00
Shane McDonald
f3669f3be6 Fix make install_collection
The version obtained from setuptools_scm is not compatible with ansible-galaxy collection install.
2021-10-19 10:26:23 -04:00
Shane McDonald
61eb99c46d Merge pull request #11253 from beeankha/collections_docs_fix_pt2
Update auth_plugin Doc Extension File to Fix Malformed Collections Docs
2021-10-18 18:07:41 -04:00
Bianca Henderson
f74a14e34f Update auth_plugin doc extension to fix malformed Collections docs 2021-10-18 11:08:17 -04:00
Shane McDonald
517f1d7991 Merge pull request #9491 from sezanzeb/awxkit-credential-file
making the cli use AWXKIT_CREDENTIAL_FILE
2021-10-13 19:05:56 -04:00
Bianca Henderson
25e69885d0 Merge pull request #11198 from sean-m-sullivan/name_or_id_workflow_node
update to allow use of id for unified job template
2021-10-13 15:19:02 -04:00
Shane McDonald
60a357eda1 Merge pull request #10906 from oweel/10829-idle_timeout_setting
Added idle_timeout setting to job settings
2021-10-13 13:16:53 -04:00
Cesar Francisco San Nicolas Martinez
d74679a5f9 Merge pull request #11244 from ansible/CFSNM-fix-minor-typo
Update test_ha.py
2021-10-13 17:04:34 +02:00
Chris Meyers
73a865073d Merge pull request #11241 from chrismeyersfsu/fix-missing-project-updates
Fix missing project updates
2021-10-13 11:03:44 -04:00
Cesar Francisco San Nicolas Martinez
4ff8c28fe4 Update test_ha.py
Fixed minor typo in node type
2021-10-13 16:46:58 +02:00
Shane McDonald
4ab2539c8a Merge pull request #11242 from shanemcd/awx-operator-ci-check
Add awx-operator CI check
2021-10-13 10:28:23 -04:00
Tiago Góes
459eb3903e Merge pull request #11208 from AlexSCorey/7741-GroupAdvanceSearchKeys
Groups Advanced Search Keys
2021-10-13 10:32:26 -03:00
chris meyers
611a537b55 add missing create partition for scm backed inv
* This will resolve missing project update job events issue
2021-10-13 07:51:40 -04:00
Shane McDonald
3a74cc5a74 Add awx-operator CI check 2021-10-12 18:59:24 -04:00
Shane McDonald
f1520e1a70 Allow for building headless mode
This will only be used in CI and maybe other places where we dont need a UI
2021-10-12 18:59:24 -04:00
Shane McDonald
727b4668c2 yamllint: ignore some gitignore'd directories 2021-10-12 18:59:24 -04:00
Shane McDonald
1287e001d8 yamllint: disable truthy rule
This rule feels very anti-Ansible
2021-10-12 18:59:23 -04:00
Shane McDonald
c9b53cf975 Refactor image_build and image_push roles
Primary changes are:

- Generalized variable names (remove "docker")
- Add explicit "push" variable rather than checking if the "registry" variable is defined.
- Allow for passing in version as build arg
2021-10-12 18:59:13 -04:00
chris meyers
64811d0b6b fix python black lint requirements 2021-10-12 17:09:30 -04:00
Alan Rominger
74af187568 Fix Makefile conditional used for docker-refresh (#11238) 2021-10-12 13:52:52 -04:00
sean-m-ssullivan
a28c023cf1 update to allow use of id for unified job template 2021-10-12 13:06:30 -04:00
Shane McDonald
cdf7fd64b2 Merge pull request #11230 from no-12/devel
Fix survey update with job_template module
2021-10-11 17:23:57 -04:00
Shane McDonald
84ffa4a5b7 Merge pull request #11189 from nntrn/pgsql-12
Change pgsql version from 10 to 12 in template for dockerfile role
2021-10-11 15:41:18 -04:00
Shane McDonald
326a43de11 Merge pull request #11231 from CastawayEGR/fix-awx-collection-spelling
fix spelling of Vault
2021-10-11 15:37:20 -04:00
Amol Gautam
07f193d8d6 Merge pull request #11226 from amolgautam25/K8s_signed_work
Changed Work Submission parameter for K8s work
2021-10-11 13:03:28 -04:00
Amol Gautam
f79a57c3e2 Changed Work Submission parameter for K8s work 2021-10-11 08:10:26 -07:00
Michael Tipton
f8319fcd02 fix spelling of Vault 2021-10-09 23:46:16 -04:00
Nico Ohnezat
815ef4c9c9 related #11229 consider previous set json_output changed in
controller_api

job_template module sets self.json_output['changed'] to true before calling create_or_update_if_needed.

Signed-off-by: Nico Ohnezat <nico@no-12.net>
2021-10-08 23:59:12 +02:00
kialam
d1800aa6d0 Merge pull request #11218 from kialam/revert-pf-upgrade
Roll back PF deps upgrade to re-enable select input typing.
2021-10-08 11:38:55 -07:00
Wambugu “Innocent” Kironji
dda940344e Merge pull request #11209 from kialam/fix-job-list-refresh
Pass configurable qs to fetchJobsById function.
2021-10-08 13:18:53 -04:00
Kersom
1fffeb430c Merge pull request #11216 from AlexSCorey/11214-DisableDefaultInstanceDelete
Disable default instance delete
2021-10-08 12:48:23 -04:00
Jeff Bradberry
7d0bbd0a4c Merge pull request #11225 from jbradberry/revert-iso-group-removal
Revert removing the old isolated groups
2021-10-08 12:38:03 -04:00
Jeff Bradberry
15fd22681d Revert removing the old isolated groups
In 4.1+ / AAP 2.1+, isolated groups should be converted into plain
instance groups, and it's desirable for the old ones to stick around
since they'll likely be tied to a bunch of job templates.  We do not
want to make the users have to reconstruct those relationships.
2021-10-08 11:53:21 -04:00
Chris Meyers
6a2826b91c Merge pull request #11088 from saito-hideki/issue/10879
Fixed Org mapping behavior with SAML when Ansible Galaxy cred does not exist
2021-10-08 10:48:11 -04:00
Jim Ladd
112111c7f9 Merge pull request #10904 from jladdjr/do_not_collect_artifact_data
do not collect artifact_data when gathering analytics
2021-10-07 22:46:00 -07:00
Alan Rominger
ed8498f43f Change search location for job private data (#11217) 2021-10-07 20:33:57 -04:00
Kia Lam
77a5bb9069 Roll back PF deps upgrade to re-enable select input typing. 2021-10-07 15:36:14 -07:00
Alex Corey
37f86803f7 Disables name field for default and controlplan instance groups 2021-10-07 15:36:25 -04:00
Tiago Góes
160858b051 Merge pull request #11206 from nixocio/ui_update
Upgrade a few ui dependencies
2021-10-07 15:55:50 -03:00
Kia Lam
68f44c01ea Rely on default qs value. 2021-10-07 09:52:33 -07:00
Alex Corey
bef8d7426f Groups Advanced search keys, and removes Clear all filters text after advanced search 2021-10-07 10:08:06 -04:00
nixocio
c758f079cd Upgrade a few ui dependencies
Upgrade axios, and ansi-to-html.
2021-10-06 22:14:59 -04:00
Shane McDonald
7e404b7c19 Merge pull request #11199 from shanemcd/auto-version
Remove VERSION files, obtain version from git tags.
2021-10-06 20:14:06 -04:00
Kia Lam
4b7faea552 Remove comments and linter-disable. 2021-10-06 13:18:47 -07:00
Sarah Akus
4ddd391033 Merge pull request #11168 from AlexSCorey/11103-AllowJinjaOnSettings
Sufrace ALLOW_JINJA_IN_EXTRA_VARS on the job settings page
2021-10-06 15:59:28 -04:00
Alan Rominger
e52416fd47 Report single node clusters as non-ha (#11212)
* Report single node clusters as non-ha

* Move test file so we can make it use the database

* Update unit test to accomidate different node types
2021-10-06 10:50:18 -04:00
Shane McDonald
f67a2d2f46 Make setup.py compatible with older pythons
This caused some annoying downstream failures I'd rather not fix right now.
2021-10-05 19:11:03 -04:00
Shane McDonald
fcdda8d7a7 Remove old test comparing VERSION files 2021-10-05 19:11:03 -04:00
Shane McDonald
1f0b936e82 Remove VERSION files, obtain version from git tags. 2021-10-05 19:11:00 -04:00
Alan Rominger
b70793db5c Consolidate cleanup actions under new ansible-runner worker cleanup command (#11160)
* Primary development of integrating runner cleanup command

* Fixup image cleanup signals and their tests

* Use alphabetical sort to solve the cluster coordination problem

* Update test to new pattern

* Clarity edits to interface with ansible-runner cleanup method

* Another change corresponding to ansible-runner CLI updates

* Fix incomplete implementation of receptor remote cleanup

* Share receptor utils code between worker_info and cleanup

* Complete task logging from calling runner cleanup command

* Wrap up unit tests and some contract changes that fall out of those

* Fix bug in CLI construction

* Fix queryset filter bug
2021-10-05 16:32:03 -04:00
Kia Lam
0f044f6c21 Pass configurable qs to fetchJobsById function. 2021-10-05 13:04:37 -07:00
Amol Gautam
4c205dfde9 Merge pull request #11133 from amolgautam25/receptor_work_sign
AWX dev environment changes for receptor work signing feature
2021-10-05 14:57:58 -04:00
Tiago Góes
d58d460119 Merge pull request #11173 from mabashian/hub-to-controller
Adds support for pre-filling EE add form name, description, and image from query params
2021-10-05 15:57:31 -03:00
Amol Gautam
24a6edef9e AWX dev environment changes for receptor work signing feature
-- Updated devel build to take most recent receptor binary
-- Added signWork parameter when sedning job to receptor
-- Modified docker-compose tasks to generate RSA key pair to use for work-signing
-- Modified docker-compose templates and jinja templates for implementing work-sign
-- Modified Firewall rules on the receptor jinja config

Add firewall rules to dev env
2021-10-05 11:41:34 -07:00
Kersom
a5485096ac Merge pull request #11200 from nixocio/ui_update_unit_tests
Update unit-tests
2021-10-05 14:29:07 -04:00
Kersom
60a5ccf70b Merge pull request #11201 from nixocio/ui_remove_console
Remove console.log
2021-10-05 14:28:42 -04:00
Marliana Lara
d93a7c2997 Reset form values when query params change 2021-10-05 13:10:33 -04:00
Alan Rominger
af5f8e8a4a Always set project sync execution_node to current host (#11204) 2021-10-05 13:08:40 -04:00
nixocio
1596c855ff Remove console.log
Remove console.log
2021-10-05 11:26:03 -04:00
nixocio
f45dd7a748 Update unit-tests
Update unit-tests mocked values, as attempt to mitigate CI failures.
2021-10-05 11:16:42 -04:00
Shane McDonald
a036363e85 Merge pull request #11195 from shanemcd/update-pip-and-setuptools
Update pip and setuptools
2021-10-04 18:50:51 -04:00
Shane McDonald
4aceea41fd Update licensce test to work with newer pip 2021-10-04 17:41:48 -04:00
Shane McDonald
7bbfcbaefd Update dev requirements to work with setuptools 58 2021-10-04 16:24:16 -04:00
Elijah DeLee
18eaa9bb92 Merge pull request #11166 from ansible/receptorctl-status-sosreport
get receptorctl status for sosreport
2021-10-04 16:13:37 -04:00
Tiago Góes
6826d5444b Merge pull request #11183 from AlexSCorey/11170-fix
Fixes Instance Group tooltip
2021-10-04 15:35:03 -03:00
Alex Corey
622ec69216 fixes tooltip 2021-10-04 14:17:13 -04:00
Shane McDonald
d38c109d49 Update pip and setuptools 2021-10-04 13:07:16 -04:00
Tiago Góes
a31b2d0259 Merge pull request #11192 from AlexSCorey/11191-fix
Fixes delete message
2021-10-04 12:39:19 -03:00
Tiago Góes
b13c076881 Merge pull request #11148 from AlexSCorey/11105-UpdatePF
Updates PF dependencies, and Instance Toggle labels
2021-10-04 12:29:36 -03:00
Alex Corey
c429a55382 Fixes delete message 2021-10-04 10:58:48 -04:00
Alex Corey
20c4b21c39 Sufrace ALLOW_JINJA_IN_EXTRA_VARS on the job settings page 2021-10-04 10:24:26 -04:00
Elijah DeLee
d3289dc688 fix typo in comment in tools/sosreport/controller.py 2021-10-04 09:45:11 -04:00
annie tran
685c0b844e Change pgsql version from 10 to 12 in template for dockerfile role 2021-10-04 06:34:16 -05:00
Shane McDonald
57c9b14198 Fix docker-compose targets 2021-10-03 13:40:26 -04:00
Shane McDonald
0736f4d166 Merge pull request #11187 from shanemcd/bump-19.4.0
AWX 19.4.0
2021-10-02 15:21:01 -04:00
Shane McDonald
fed94b531d Merge pull request #11163 from gbaychev/patch-1
Update websockets.md
2021-10-02 15:17:03 -04:00
Shane McDonald
43a77e8667 Clean up unused parts of Makefile 2021-10-02 14:22:14 -04:00
Shane McDonald
637dc3844d Bump version 2021-10-02 14:15:54 -04:00
Jim Ladd
815a45cf2f call 'work cancel' on inactive controller jobs 2021-10-01 12:55:06 -07:00
Sarah Akus
0b66b61dd6 Merge pull request #11184 from akus062381/user-date-detail-revised
change locator- UserDateDetail
2021-10-01 15:07:24 -04:00
akus062381
7c011a1796 fix commit problem, user date detail
fix commit problem, user date detail
2021-10-01 14:51:03 -04:00
Sarah Akus
bf8859f401 Merge pull request #11171 from nixocio/ui_issue_11169
Fix subform for AAP for inventory source
2021-10-01 12:46:02 -04:00
Sarah Akus
c14d5ec59e Merge pull request #11158 from AlexSCorey/11137-FixSurveyIntegerValidation
Properly validates Integer Survey Question with min value of 0
2021-10-01 12:36:19 -04:00
Elijah DeLee
6d850e031a Merge pull request #11175 from ansible/receptor-sos
don't collect keys with sosreport
2021-09-30 10:09:16 -04:00
Tiago Góes
38af9e2d42 Merge pull request #11156 from nixocio/ui_issue_11150
Fix translation for instance list
2021-09-30 10:16:38 -03:00
nixocio
a3d7901d5f Fix translation for instance list
Fix translation for instance list. Issue was just visible on production
build.

See:https://github.com/ansible/awx/issues/11150
2021-09-30 09:01:01 -04:00
Elijah DeLee
54b3e2f285 don't collect keys with sosreport 2021-09-30 08:58:16 -04:00
mabashian
d0a13cb12a Adds test coverage for parsing and prefilling form fields from query params on EE add form 2021-09-29 16:59:16 -04:00
nixocio
003bf29dce Fix subform for AAP for inventory source
Fix subform for AAP for inventory source

See: https://github.com/ansible/awx/issues/11169
2021-09-29 16:53:48 -04:00
mabashian
71c72f74a1 Add support for name and description query params on ee add 2021-09-29 16:45:07 -04:00
Alex Corey
adaa24a562 Properly validates Integer Survey Question with min value of 0 2021-09-29 15:02:22 -04:00
mabashian
ad24fe7017 Remove cred from potential hub params 2021-09-29 13:57:27 -04:00
mabashian
e5578a8ef3 Fix bad merge conflict resolution 2021-09-29 13:55:30 -04:00
Elijah DeLee
3a40d5e243 get receptorctl status for sosreport
I presume the logs also get collected from journalctl but I'm not sure
2021-09-29 11:24:49 -04:00
Marliana Lara
8e34898b4e Redirect with query params and update EE form with hub image data 2021-09-29 11:22:56 -04:00
Bianca Henderson
7eefa897b3 Merge pull request #11070 from beeankha/receptor_related_docs_changes
Update Tasks and Clustering Doc Files
2021-09-29 11:17:47 -04:00
beeankha
4c7c89b410 Update wording from 'node' to 'instance' 2021-09-29 10:12:19 -04:00
beeankha
caafa55c35 Update clustering.md doc to remove some installer-related info 2021-09-29 10:05:53 -04:00
beeankha
7776d426ac Delete receptor_mesh.md file, update docker-compose README with new cluster info 2021-09-29 10:05:53 -04:00
beeankha
2d87ccface Update Tasks doc file with Receptor work unit information 2021-09-29 10:05:53 -04:00
Sarah Akus
b9131b9e8b Merge pull request #11157 from nixocio/ui_issue_11113
Redirect system path to management on jobs URL
2021-09-29 10:05:06 -04:00
Alan Rominger
7c9626b0e7 Fix bug that would run --worker-info health checks on control or hybrid nodes (#11161)
* Fix bug that would run health check on control nodes

* Prevent running execution node health check against main cluster nodes
2021-09-29 09:34:22 -04:00
Georgi Baychev
1338aef2bd Update websockets.md
- specify that the websockets are not meant for external usage/3rd party clients (see #10764)
- add a the missing slash at the '/websocket/' URL
2021-09-29 13:55:12 +02:00
Bianca Henderson
b9ecf389c2 Merge pull request #11151 from sean-m-sullivan/simplify_utils
simplify module utils python to single file
2021-09-28 17:36:02 -04:00
Bianca Henderson
75a873079d Merge pull request #11135 from sean-m-sullivan/workflow_node_id
add ability to lookup unified job template by org
2021-09-28 13:28:36 -04:00
Satoe Imaishi
4824153cd9 Merge pull request #11154 from simaishi/venv_add_pbr
Fix rpm 'offline' build (add pbr to venv and change receptorctl wheel file name)
2021-09-28 12:06:23 -04:00
sean-m-ssullivan
5b28e7b397 simplify module utils files 2021-09-28 11:43:28 -04:00
Satoe Imaishi
f3f781917a Skip pbr license check if ansible-runner isn't a released version 2021-09-28 11:07:30 -04:00
nixocio
4398c7c777 Redirect system path to management on jobs URL
When user attempts to access `/jobs/system/66` redirect to
`/jobs/management/66`.

This will catch management jobs notifications, for instance, and redirect to
the proper URL.

See:#11113
2021-09-28 10:23:57 -04:00
Satoe Imaishi
b6179c6073 receptorctl whl with version number 2021-09-28 08:27:12 -04:00
sean-m-ssullivan
dd4943310d simplify module utils files 2021-09-27 19:35:22 -04:00
Satoe Imaishi
7df6f8d88c Add pbr to venv temporarily 2021-09-27 18:02:47 -04:00
sean-m-ssullivan
c026790f55 add ability to lookup unified job template by org 2021-09-27 17:31:33 -04:00
Alex Corey
0b0d049071 Updates PF dependencies, and Instance Toggle labels 2021-09-27 17:26:39 -04:00
Kersom
87105a654c Merge pull request #11147 from kialam/fix-logout-react-console-error
Wrap `setAuthRedirectTo` in useEffect.
2021-09-27 16:57:30 -04:00
Kia Lam
32651db4e9 Wrap setAuthRedirectTo in useEffect. 2021-09-27 10:58:49 -07:00
Marcelo Moreira de Mello
270f6c4abd Merge pull request #11143 from tchellomello/fix_streamtls_when_not_present
Fixed logic to avoid tracebacks when node_name is invalid
2021-09-27 11:49:11 -04:00
Alan Rominger
3664cc3369 Disable autodiscovery except for docker-compose (#11142) 2021-09-27 11:36:11 -04:00
Marcelo Moreira de Mello
2204e03123 Fixed logic to avoid tracebacks when node_name is invalid 2021-09-27 11:28:28 -04:00
Sarah Akus
7b6befa3d2 Merge pull request #11134 from nixocio/ui_issue_11127
Do not display EE if a job was canceled
2021-09-27 11:10:36 -04:00
Sarah Akus
84bc91defd Merge pull request #11132 from nixocio/ui_issue_6302
Break lines for long strings on main lists
2021-09-27 11:05:14 -04:00
nixocio
2dca92c788 Do not display EE if a job was canceled
Do not display EE if a job was canceled. Since the API is returning null
for the value of the EE for this particular scenario.

See: https://github.com/ansible/awx/issues/11127
2021-09-27 08:33:50 -04:00
Marcelo Moreira de Mello
76dc22cd06 Merge pull request #11118 from tchellomello/ensure_controller_node_is_assigned_prj_updates
Project updates must run on controller nodes
2021-09-25 23:19:11 -04:00
Marcelo Moreira de Mello
6d4b4cac37 Project updates must run on controller nodes
For project updates jobs triggered due a job template run,
we must ensure that project_update job to run on at the same
controller which dispatched the original job template, otherwise
the job might fail for being unable to find the playbook YAML file.
2021-09-25 23:05:45 -04:00
Alan Rominger
3fc63489f1 Filter controller_node selection to online nodes (#11120) 2021-09-24 23:01:32 -04:00
nixocio
e8cd8c249c Break lines for long strings on main lists
Break lines for long strings on main lists to avoid horizontal
scrolling.

Main goal of this change is to avoid actions items to be hidden on the
main lists.

See: https://github.com/ansible/awx/issues/6302
2021-09-24 15:38:25 -04:00
Marcelo Moreira de Mello
471f47cd9e Merge pull request #11093 from ansible/receptor_control_service_tls
Introduce the control-service TLS support on receptor
2021-09-24 12:26:19 -04:00
Rebeccah Hunter
e5dbb592fa Merge pull request #11074 from rebeccahhh/no_duplicate_uuids
prevent duplicate UUIDs from being created and allow users to update hostnames based on uuid
2021-09-24 11:52:55 -04:00
Shane McDonald
44466a3e76 Merge pull request #11077 from shanemcd/nightly-receptorctl
Install receptorctl from new nightly url
2021-09-24 11:40:29 -04:00
Wambugu “Innocent” Kironji
d6ef84e9e2 Merge pull request #11122 from nixocio/ui_issue_10942
Update empty survey list
2021-09-24 10:40:41 -04:00
Shane McDonald
c4d8485c81 Update license test to work with http(s) urls in requirements files 2021-09-24 10:16:11 -04:00
Shane McDonald
dbb1a0c733 Install receptorctl from new nightly url
We ran into problems with our offline builds with our usage of PBR + subdirectory
2021-09-24 09:59:12 -04:00
Alan Rominger
b5dee61e57 Delete wording that we have reversed position on (#11129) 2021-09-24 09:38:48 -04:00
Kersom
2c7d9320e2 Merge pull request #11125 from nixocio/ui_remove_component
Remove unused component VariablesInput
2021-09-23 17:10:01 -04:00
Alex Corey
fd3a82d430 Merge pull request #11123 from AlexSCorey/11028-MeshFix
Removes receptor instances from select option on metrics screen
2021-09-23 16:16:38 -04:00
nixocio
3a776ccbff Update empty survey list
Update empty survey list to be as the remaining lists.

See: https://github.com/ansible/awx/issues/10942
2021-09-23 15:32:15 -04:00
kialam
f96ed11a87 Merge pull request #11107 from kialam/fix-10785-ee-revert-button
Wrap ExecutionEnv Lookup in SettingGroup component.
2021-09-23 14:17:45 -04:00
Alex Corey
86f8ced486 Removes receptor instances from select option on metrics screen 2021-09-23 14:17:26 -04:00
nixocio
940f055412 Remove unused component VariablesInput
Remove unused component VariablesInput

See: https://github.com/ansible/awx/pull/11102/files
2021-09-23 13:54:56 -04:00
Kia Lam
d7f1f0c7e6 Remove validation and unused vars for EE Lookup. 2021-09-23 10:23:51 -07:00
Marcelo Moreira de Mello
045785c36f Refactored get_conn_type() method to use Enum 2021-09-23 10:51:50 -04:00
Marcelo Moreira de Mello
45600d034d Initial StreamTLS support for receptor nodes 2021-09-23 10:50:17 -04:00
Alex Corey
33c7f0b5fc Merge pull request #11104 from AlexSCorey/8826-ErroneousFormValidation
Prevents form validation on cancel button click
2021-09-23 10:01:35 -04:00
Alan Rominger
62e9e7ea80 Avoid setting controller_node to an execution node for container jobs (#11117) 2021-09-23 09:16:10 -04:00
Kersom
a75c10f447 Merge pull request #11115 from nixocio/ui_issue_11111
Fix broken link for inventory
2021-09-22 16:47:29 -04:00
nixocio
ee4b47595a Fix broken link for inventory
Fix broke link for inventory

See: https://github.com/ansible/awx/issues/11111
2021-09-22 14:43:52 -04:00
Kersom
9be8fba63d Merge pull request #11102 from AlexSCorey/11099-ExtraVarsPopOut
Adds Popout for extra vars on Job Details view
2021-09-22 13:17:14 -04:00
Alex Corey
15f41a0f16 Prevents form validation on cancel button click 2021-09-22 11:09:39 -04:00
Kia Lam
f06eb5e2f1 Wrap ExecutionEnv Lookup in SettingGroup component. 2021-09-21 18:51:48 -07:00
Rebeccah
a9f4011a45 defensive code for getting instance added, also simplified nested if
statements, rewrote some comments add a logger warning that the instance is being grabbed by the hostname and not the UUID
2021-09-21 16:54:11 -04:00
Rebeccah
55f2125a51 if the user provides a uuid and it exists, allow that to tie to the instance, which allows the user to update the instance based on the UUID (includeding updating the hostname) should they choose to do so. 2021-09-21 16:54:11 -04:00
Sarah Akus
b41f90e7d4 Merge pull request #11090 from AlexSCorey/10952-AddHealthCheckOnInstancesList
Adds Health Check functionality to instance list
2021-09-21 16:38:22 -04:00
Wambugu “Innocent” Kironji
7c707ede2b Merge pull request #11096 from AlexSCorey/10945-DisplayCurrentConvergenceData
Fixes convergence data value on node edit mode
2021-09-21 16:26:16 -04:00
Alex Corey
4df9f9eca0 Adds Health Check functionality to instance list 2021-09-21 16:10:26 -04:00
Alex Corey
6af27fffbc Adds popout for extra vars on Job Details view 2021-09-21 14:42:08 -04:00
Alex Corey
a7ed9c5ff6 Fixes convergence data value on node edit mode 2021-09-21 14:39:09 -04:00
Sarah Akus
51b45c4fac Merge pull request #11097 from nixocio/ui_issue_11061
Fix worfklow node info
2021-09-21 14:35:52 -04:00
nixocio
313de35e60 Fix worfklow node info
Fix workflow node info.

See: https://github.com/ansible/awx/issues/11061
Also: https://github.com/ansible/awx/issues/10628
2021-09-20 15:42:31 -04:00
Alan Rominger
0ac3a377fd Make some needed updates to docker-refresh target (#11089) 2021-09-17 09:11:52 -04:00
Alan Rominger
1319fadc60 Fix overwrite bug where hosts with no instance ID var are re-created (#10910)
* Write tests to assure air-tightness of overwrite

* Candidate fix for group overwrite air-tightness

* Another proposed fix for the id mapping

* Further double down on tracking old instance_id

* Separate unchanging data case and fix some test issues

* parametrize final confusion test

* cut down some more on test cases and fix bug with prior fix

* Rewrite of _delete_host code sharing with update method

This is a start-to-finish rewrite of the host overwrite bug fix
this method is much more conservative,
it does this by keeping the overall code structure where hosts
are deleted before host updates are made

To fix the bug, we share code between the method that deletes hosts
and the method that updates the hosts
A data structure is created and passed to both methods

By having both methods use the same data structure which maps
the in-memory hosts to DB hosts, we assure that the deletions
will ONLY delete hosts that will not be updated
2021-09-16 15:29:57 -04:00
Bianca Henderson
181bda51ce Merge pull request #11081 from sean-m-sullivan/schedule_credentials
add credentials option to schedules
2021-09-16 13:19:58 -04:00
Alan Rominger
e914c23c42 Pass --delete flag to worker for execution node cleanup (#11078)
* Pass --delete flag to worker for execution node cleanup

* Remove the pdd_wrapper_ directory
2021-09-16 11:21:41 -04:00
Bianca Henderson
c1587b25b8 Merge pull request #11064 from beeankha/new_way_to_auth_for_exec_nodes
Enable Jobs to Run on Execution-Only Nodes Via EEs from Protected Registries
2021-09-16 11:20:33 -04:00
Hideki Saito
9e74ac24fa Fixed Org mapping behavior with SAML when Ansible Galaxy cred does not exist
- Fixes #10879
- Fixes ansible/tower#5061

Signed-off-by: Hideki Saito <saito@fgrep.org>
2021-09-16 23:25:50 +09:00
beeankha
48eb06f320 Add verify_ssl to container_auth_data params 2021-09-16 09:49:53 -04:00
Alex Corey
65ba87e71f Merge pull request #11069 from AlexSCorey/10951-InstanceDetailsandHealthCheck
This adds Instance Details view
2021-09-16 09:38:27 -04:00
sean-m-ssullivan
f92924d57e add credentials option to schedules 2021-09-16 08:47:00 -04:00
Alex Corey
eeb0feabc0 Adds the Instance Details view with the health check functionality 2021-09-15 14:20:30 -04:00
beeankha
ac8b49b39d Change the way auth info is passed to Runner for EEs pulled from protected registries 2021-09-15 08:49:28 -04:00
Jim Ladd
1b50db26b6 Explicitly pass in UUID to get_or_register
Co-authored-by: Alan Rominger <arominge@redhat.com>
2021-09-14 10:58:29 -07:00
sezanzeb
cbe612baa5 add credential file support
Signed-off-by: sezanzeb <proxima@sezanzeb.de>
2021-09-12 17:58:49 +02:00
Alex Corey
1f34d4c134 Merge pull request #11029 from AlexSCorey/10070-translateMetrics
Translates the UI strings on the metrics pages
2021-09-10 14:46:14 -04:00
Christian Adams
f864335463 Merge pull request #11066 from ansible/i18n_devel_translations
UI translation strings for devel branch
2021-09-10 14:30:58 -04:00
Alex Corey
47970d3455 Translates the UI strings on the metrics pages 2021-09-10 11:57:38 -04:00
Bianca Henderson
6cdaacdda3 Merge pull request #11062 from john-westcott-iv/collection_version_change
Collection version change
2021-09-10 10:13:37 -04:00
beeankha
9b66bda8b9 Fix pep8 error 2021-09-10 09:20:44 -04:00
Kersom
ef354ca1e6 Merge pull request #11065 from nixocio/ui_linguirc
Minor update linguirc
2021-09-10 08:54:35 -04:00
John Westcott IV
515c3450c2 Fixing linting issue 2021-09-10 08:46:41 -04:00
John Westcott IV
5607c350cd Removing parens 2021-09-10 08:46:41 -04:00
John Westcott IV
b9758f5c1a Adding unit test for no header response 2021-09-10 08:46:41 -04:00
John Westcott IV
aad432aaa3 Changing to Version instead of Type 2021-09-10 08:46:41 -04:00
John Westcott IV
d4971eb7b7 Preventing error if we were unable to get an API version 2021-09-10 08:46:41 -04:00
Christian M. Adams
7860eb529f Localization: fix dynamic vars in fr .po files 2021-09-09 19:08:04 -04:00
nixocio
49c2a38437 Minor update linguirc
Minor update linguirc
2021-09-09 17:57:29 -04:00
ansible-translation-bot
d4bf238173 UI translation strings for devel branch 2021-09-09 17:05:23 -04:00
Sarah Akus
c085397bcb Merge pull request #11023 from nixocio/ui_issue_10933
Show button to cancel inventory source sync
2021-09-09 14:16:07 -04:00
nixocio
58fab2530f Show button to cancel inventory source sync
Show button to cancel inventory source sync.

See: https://github.com/ansible/awx/issues/10933
Also: https://github.com/ansible/awx/issues/10991
2021-09-09 14:04:10 -04:00
Sarah Akus
287b32870e Merge pull request #11014 from kialam/add-node-type-to-associate-modal
Add instance node type to associate modal.
2021-09-09 10:17:12 -04:00
Alan Rominger
46ac9506e6 Assure consistent ordering with default IG first (#11034)
* Assure consistent ordering with default IG first

* Write conditional a little more defensively to pass tests
2021-09-08 11:11:46 -04:00
Elijah DeLee
19ccfcff9a Merge pull request #10988 from ansible/more-receptor-sos
List dir where receptor socket should be
2021-09-08 10:43:48 -04:00
Christian Adams
f8a08c8a5e Merge pull request #11035 from rooftopcellist/build_app_image_docs
Update image variable name for awx-operator app image docs
2021-09-08 09:32:14 -04:00
Christian M. Adams
6f7fe8f9f9 Update image variable name for awx-operator app image docs
Signed-off-by: Christian M. Adams <chadams@redhat.com>
2021-09-07 17:07:47 -04:00
Kersom
86b41a4887 Merge pull request #11011 from nixocio/ui_issue_10971
Add strings to be translated
2021-09-03 17:12:14 -04:00
Sarah Akus
3786693078 Merge pull request #10978 from AlexSCorey/10973-fix
Fixes translation issue on Schedule Form
2021-09-03 16:42:23 -04:00
Alan Rominger
6a17e5b65b Allow manually running a health check, and make other adjustments to the health check trigger (#11002)
* Full finalize the planned work for health checks of execution nodes

* Implementation of instance health_check endpoint

* Also do version conditional to node_type

* Do not use receptor mesh to check main cluster nodes health

* Fix bugs from testing health check of cluster nodes, add doc

* Add a few fields to health check serializer missed before

* Light refactoring of error field processing

* Fix errors clearing error, write more unit tests

* Update health check info in docs

* Bump migration of health check after rebase

* Mark string for translation

* Add related health_check link for system auditors too

* Handle health_check cluster node timeout, add errors for peer judgement
2021-09-03 16:37:37 -04:00
Elijah DeLee
169c0f6642 Merge pull request #11022 from kdelee/try-localhost
Set python to ansible_playbook_python on hosts
2021-09-03 15:48:33 -04:00
Elijah DeLee
054569da70 emulate workaround present in demo inventory
see 9d000a76de

This change works around the fact that the presumed correct python3 for rhel8 (which the EE is based on)
is not the python3 that ansible-playbook is using, and is not where the python dependencies are installed.
2021-09-03 15:21:34 -04:00
Elijah DeLee
4a6ab622df Update inventory.py 2021-09-03 15:20:10 -04:00
nixocio
07cc75f6d4 Add strings to be translated
Add strings to be translated

See: https://github.com/ansible/awx/issues/10971
2021-09-03 15:15:12 -04:00
Bianca Henderson
7fc8775654 Merge pull request #11018 from beeankha/node_type_on_ping
Add Node Type Information to /api/v2/ping Endpoint
2021-09-03 15:01:49 -04:00
beeankha
41a6473782 Sort instance groups by name regardless of upper/lower case 2021-09-03 13:52:12 -04:00
Jim Ladd
f39834ad82 pass uuid to Instance.create 2021-09-03 10:05:15 -07:00
Jim Ladd
bdb13343bb remove unused import 2021-09-03 10:05:15 -07:00
Jim Ladd
262cd3c695 set default uuid 2021-09-03 10:05:15 -07:00
Jim Ladd
f02099e8b7 provision_instance should create new uuid if needed
.. instead of default to current system's UUID

related: #10990
2021-09-03 10:05:15 -07:00
Kersom
7bf3ee69ef Merge pull request #10987 from nixocio/ui_issue_9013
Add websockets to Inventory Source Details
2021-09-03 12:37:53 -04:00
Kia Lam
41e837d1e2 Properly mark strings for translation. 2021-09-03 12:36:04 -04:00
beeankha
2090e46ac2 Add node_type to api/v2/ping/ endpoint 2021-09-03 11:25:05 -04:00
Kersom
f09ee33e6c Merge pull request #10994 from nixocio/ui_issue_10966
Add string `Filter By` to be translated
2021-09-03 11:19:40 -04:00
Alan Rominger
22782f8c5f Add wording about expectations for enabled status and default group (#10993)
* Add wording about expections for enabled status and default group

* fix pluralization

Co-authored-by: Alex Corey <acorey@redhat.com>

* Correct grammar mistake

Co-authored-by: Alex Corey <acorey@redhat.com>
2021-09-03 10:35:29 -04:00
Alex Corey
e61e7df54e Moved the entirety of the field label to the parent component to improve translation 2021-09-03 09:57:19 -04:00
Jake McDermott
baf37e94eb Merge pull request #11003 from AlexSCorey/5252-tower-Settings-deprecation
Adds deprecation banner
2021-09-03 09:04:11 -04:00
Kia Lam
bba2a264ea Add instance node type to associate modal. 2021-09-02 20:01:03 -04:00
Alex Corey
324ca7fe72 Merge pull request #11013 from nixocio/ui_issue_10977
Display finished date once the job is finished
2021-09-02 16:44:15 -04:00
nixocio
fb5394e31c Display finished date once the job is finished
Display finished date once the job is finished.

See: https://github.com/ansible/awx/issues/10977
2021-09-02 13:27:02 -04:00
nixocio
53baea4c6c Add websockets to Inventory Source Details
Add websockets to Inventory Source Details

See: https://github.com/ansible/awx/issues/9013
2021-09-02 10:35:08 -04:00
Alex Corey
35a51b393a Adds deprecation banner 2021-09-02 10:14:00 -04:00
kialam
9ee9de76b5 Merge pull request #11001 from kialam/fix-instance-list-name-sort
Sort instance by hostname instead of name.
2021-09-02 09:51:56 -04:00
Alex Corey
ae15dcaf0b Merge pull request #10958 from AlexSCorey/10557-NotificationValidation
Fixes validation issues associated with the Notification Form
2021-09-02 09:49:02 -04:00
Alan Rominger
eb0528c157 dev environment - change location of receptor socket and sync awx and receptor nodes function (#11005)
* Change the location of the receptor socket

to /var/run/awx-receptor, to match what the installer is currently doing.

* Sync awx and receptor nodes for control socket

Co-authored-by: Jeff Bradberry <jeff.bradberry@gmail.com>
2021-09-02 09:18:25 -04:00
nixocio
764089e493 Add string Filter By to be translated
Add string `Filter By` to be translated

See:https://github.com/ansible/awx/issues/10966
2021-09-02 09:02:56 -04:00
Kersom
77e704cef1 Merge pull request #11007 from rooftopcellist/rm-add-locale
Remove deprecated lingui add-locale cmd
2021-09-02 08:52:09 -04:00
Christian M. Adams
59ce1bba16 Remove deprecated lingui add-locale cmd 2021-09-01 16:53:30 -04:00
Alan Rominger
1d3a36d821 Fix the hostname of execution nodes in dev environment (#10992) 2021-09-01 13:35:39 -04:00
Kia Lam
dc0d74ca2c Sort instance by hostname instead of name. 2021-09-01 13:20:25 -04:00
kialam
ef36d7c87f Merge pull request #10927 from kialam/feature-10853-control-node-read-only
Disable checkbox for instances with node type control.
2021-09-01 09:30:37 -04:00
Jeff Bradberry
81fe39f060 Merge pull request #10929 from ansible/validate-control-only-nodes
Validate that control-only Instance nodes cannot change IG membership
2021-09-01 09:24:40 -04:00
Alan Rominger
5a6e9a06e2 Exclude control-only nodes from IG policy calculations (#10985)
* Exclude control-only nodes from IG policy calculations

Also, as a reverse to this, exclude execution-only nodes from
  the calculations if the group in question is the controlplane

* Incorporate review comments
2021-09-01 08:09:46 -04:00
Chris Meyers
9083425c24 Merge pull request #10940 from chrismeyersfsu/doc-debug-slow-api
add docs for debugging slow api endpoint
2021-08-31 16:30:21 -04:00
Chris Meyers
010f5031a7 grammar 2021-08-31 16:17:48 -04:00
Alex Corey
40e5b70495 Fixes validation issues associated with the Notification Form 2021-08-31 14:35:50 -04:00
Marcelo Moreira de Mello
9588ff3b4f Merge pull request #10965 from tchellomello/fix_ldap_dn
Associates ldap_dn on a first User() login
2021-08-31 14:25:26 -04:00
Sarah Akus
30cf483357 Merge pull request #10982 from keithjgrant/json-deterministic-order
Ensure deterministic order of JSON serializing for misc auth settings
2021-08-31 14:14:19 -04:00
Sarah Akus
4d1fa4d262 Merge pull request #10959 from AlexSCorey/10642-MisalignedTableHeaders
Fixes misalignment on template list and advanced search Key dropdown bug
2021-08-31 13:08:46 -04:00
Elijah DeLee
ac40449d6e List dir where receptor socket should be
This is for adding more info to the sos report
2021-08-31 12:52:34 -04:00
Alan Rominger
dc4b014d12 Make status command in error handling cleaner (#10823) 2021-08-31 12:02:39 -04:00
Kersom
d129928e42 Merge pull request #10920 from nixocio/ui_issue_10827
Update Workflow Node Convergence
2021-08-31 11:05:01 -04:00
Alan Rominger
573b2bc44f Redefine execution plane (#10979) 2021-08-31 10:33:14 -04:00
Tiago Góes
c095f0fc19 Merge pull request #10909 from keithjgrant/6613-job-output-collapsing
Refactor Job Output component
2021-08-31 10:43:22 -03:00
Shane McDonald
ae06e9cb14 Merge pull request #10981 from kdelee/receptor_sos
Capture /etc/receptor in sos report
2021-08-30 19:56:11 -04:00
Keith J. Grant
73af95f55e set a deterministic order of JSON serializing for misc auth settings 2021-08-30 16:26:38 -07:00
nixocio
64d9a7983b Update Workflow Node Convergence
Update when `All` is displayed when editing the workflow node.

See: https://github.com/ansible/awx/issues/10827
2021-08-30 17:34:12 -04:00
Elijah DeLee
f6d14564a2 Capture /etc/receptor in sos report
this will help with debugging so we can know what receptor's configuration was
at the time the sos report was collected
2021-08-30 17:04:35 -04:00
Shane McDonald
6c266b47e6 Merge pull request #10964 from shanemcd/more-ansible-please
Automate release process and changelog generation
2021-08-30 16:02:15 -04:00
Jeff Bradberry
a2b984a1a5 Validate that control-only Instance nodes cannot change IG membership 2021-08-30 16:00:23 -04:00
Shane McDonald
0a7945a911 Remove unnecessary usage of set_fact. Thanks @samdoran! 2021-08-30 15:12:39 -04:00
Shane McDonald
9c3e78443b Hide the ugly parts in a custom action 2021-08-30 15:12:15 -04:00
Alan Rominger
68f79a1f3a Always use controlplane as project update backup IG (#10949)
* Always use controlplane as project update backup IG

Before, this was done conditionally to container_group jobs
this logic changes it so that controlgroup will always be a
firm backstop for project updates

* Code a little more defensively to make unit tests pass

* Fix unit tests
2021-08-30 14:23:09 -04:00
Jake McDermott
b00e5876d4 Merge pull request #10972 from quasd/devel
Check dynamic_input fields also with has_input()
2021-08-30 14:09:10 -04:00
Alex Corey
7481d20261 Fixes misalignment on template list and advanced search Key dropdown bug 2021-08-30 13:15:05 -04:00
quasd
637d6173bc Check dynamic_input fields also with has_inputs() - Fixes,
using credential plugins in Container Registry credential,
with execution environments

Signed-off-by: quasd <qquasd@gmail.com>
2021-08-30 16:10:34 +03:00
Marcelo Moreira de Mello
e23e634974 Associate ldap_dn on a first User() login
To avoid calling the user.save() on every single login (PR#9703)
we can check if the user.profile is available. For new users,
accessing the user.profile throws an ValueError exception which
is capture on this fix.

Example:
----
>>> _ = user.profile
*** ValueError: save() prohibited to prevent data loss due to unsaved related object 'user'.
>>> User.objects.filter(username=user.username).count()
0

This way, the user.save() gets called for brand users and will get the
ldap_dn associated as expected.
2021-08-29 22:02:00 -04:00
Shane McDonald
1c65fbaae3 Update old changelog document to point to releases page. 2021-08-29 15:54:00 -04:00
Shane McDonald
dc0cc0f910 Automate release process and changelog generation 2021-08-29 13:58:51 -04:00
Alan Rominger
424dbe8208 Use ansible-runner imports for cpu and memory calculation (#10954)
* Use ansible-runner imports for cpu and memory calculation

* Fix bug with capacity and memory adjustment
2021-08-27 21:46:53 -04:00
Alex Corey
db34423af8 Merge pull request #10950 from AlexSCorey/10947-fix
Fixes erroneous pluralization from rrule
2021-08-27 16:20:05 -04:00
Alex Corey
ca76f4db0c Fixes erroneous pluralization from rrule 2021-08-27 15:56:28 -04:00
Alan Rominger
711e5e09ba Delete images by id instead of tag in docker-refresh (#10957) 2021-08-27 11:51:58 -04:00
Alex Corey
6001bd5446 Merge pull request #10921 from AlexSCorey/tower_5194_fix
Properly marks string for translation and removes unused component
2021-08-26 16:43:52 -04:00
Alex Corey
02f60467d7 Properly marks string for translation and removes unused component 2021-08-26 16:23:32 -04:00
Sarah Akus
cdce745c55 Merge pull request #10798 from keithjgrant/7834-advanced-search-fix
Only allow legal/logical match types in advanced search
2021-08-26 14:39:40 -04:00
Jim Ladd
467a37f8fe use settings.DEFAULT_EXECUTION_QUEUE_NAME in lieu of default 2021-08-26 11:15:14 -07:00
Jim Ladd
88a6412b54 only need to update IG's policy_instance_list field 2021-08-26 11:15:14 -07:00
Jim Ladd
502eaf9fb9 handle case where default IG does not exist
* also, only add discovered execution node to default group
  if `register`-ing the node actually resulted in a confirmed
  change
2021-08-26 11:15:14 -07:00
Jim Ladd
de8eab0434 inspect_execution_nodes should *not* block when retreiving lock
* would otherwise hold up cluster heartbeat task
* furthermore, only really need one node to run through
  `inspect_execution_nodes` each interval
2021-08-26 11:15:14 -07:00
Jim Ladd
f317fca9e4 add auto-discovered nodes to default IG
* add advisory_lock to avoid IG update race logic
* update IG by way of policy_instance_list
2021-08-26 11:15:14 -07:00
Jim Ladd
561fc289fb disable discovered instances by default 2021-08-26 11:15:14 -07:00
Jim Ladd
77933e97c0 create default IG when bringing up dev env 2021-08-26 11:15:14 -07:00
Alan Rominger
ee4792dbf8 Add an option to create a cluster with control-only nodes (#10946) 2021-08-26 13:37:13 -04:00
Kia Lam
cde0df937f Filter out instances with node_type equal to 'control'. 2021-08-26 12:47:43 -04:00
Alan Rominger
daf4310176 Clean up work_type processing and fix execution vs control capacity (#10930)
* Clean up added work_type processing for mesh_code branch

* track both execution and control capacity

* Remove unused execution_capacity property

* Count all forms of capacity to make test pass

* Force jobs to be on execution nodes, updates on control nodes

* Introduce capacity_type property to abstract some details out

* Update test to cover all job types at same time

* Register OpenShift nodes as control types

* Remove unqualified consumed_capacity from task manager and make unit tests work

* Remove unqualified consumed_capacity from task manager and make unit tests work

* Update unit test to execution vs control TM logic changes

* Fix bug, else handling for work_type method
2021-08-26 07:24:14 -04:00
Alex Corey
fb0e55fd1b Merge pull request #10934 from AlexSCorey/10925-WrongDeleteModal
Fixes issue where the wrong text appeared in modal
2021-08-25 11:45:20 -04:00
Alex Corey
2e5ef22585 Fixes issue where the wrong text appeared in modal 2021-08-25 09:27:17 -04:00
Chris Meyers
8e043b139a add docs for debugging slow api endpoint 2021-08-25 09:09:19 -04:00
Alan Rominger
e7dbe90cb5 Merge pull request #10727 from ansible/mesh_code
Code changes to support execution-only nodes in receptor mesh
2021-08-24 13:39:19 -04:00
Alan Rominger
42484cf98d Obtain receptor sockfile from the receptor.conf file (#10932) 2021-08-24 11:20:21 -04:00
Shane McDonald
274e487a96 Attempt to surface streaming errors that were being eaten (#10918) 2021-08-24 10:33:00 -04:00
Alan Rominger
940c189c12 Corresponding AWX changes for runner --worker-info schema update (#10926) 2021-08-24 08:41:36 -04:00
Alan Rominger
c3ad479fc6 Minor tweaks for the mesh_code branch from review (#10902) 2021-08-24 08:41:35 -04:00
Alan Rominger
928c35ede5 Model changes for instance last_seen field to replace modified (#10870)
* Model changes for instance last_seen field to replace modified

* Break up refresh_capacity into smaller units

* Rename execution node methods, fix last_seen clustering

* Use update_fields to make it clear save only affects capacity

* Restructing to pass unit tests

* Fix bug where a PATCH did not update capacity value
2021-08-24 08:41:35 -04:00
beeankha
1a9fcdccc2 Change place where controller node is being looked for in the task manager 2021-08-24 08:41:35 -04:00
Alan Rominger
3b1e40d227 Use the ansible-runner worker --worker-info to perform execution node capacity checks (#10825)
* Introduce utilities for --worker-info health check integration

* Handle case where ansible-runner is not installed

* Add ttl parameter for health check

* Reformulate return data structure and add lots of error cases

* Move up the cleanup tasks, close sockets

* Integrate new --worker-info into the execution node capacity check

* Undo the raw value override from the PoC

* Additional refinement to execution node check frequency

* Put in more complete network diagram

* Followup on comment to remove modified from from health check responsibilities
2021-08-24 08:41:35 -04:00
Alan Rominger
4e84c7c4c4 Use the existing get_receptor_ctl method (#10813) 2021-08-24 08:41:35 -04:00
Alan Rominger
f47eb126e2 Adopt the node_type field in receptor logic (#10802)
* Adopt the node_type field in receptor logic

* Refactor Instance.objects.register so we do not reset capacity to 0
2021-08-24 08:41:34 -04:00
Alan Rominger
5d4ab13386 Add topology of docker-compose to docs, remove old mount (#10773) 2021-08-24 08:41:34 -04:00
Alan Rominger
b53d3bc81d Undo some things not compatible with hybrid node hack (#10763) 2021-08-24 08:41:34 -04:00
Alan Rominger
46ccc58749 Make the AWX nodes fully connected in the development environment (#10758) 2021-08-24 08:41:34 -04:00
Alan Rominger
289beb85d2 Add developer docs for incoming receptor mesh features (#10747)
* Add developer docs for incoming receptor mesh features

* Additional wording about the receptor mesh process

* Wrap up docs feedback changes and polishing

* Add in way more terminology introductions, delete statement about past

* Fix typo around OCP-incluster type
2021-08-24 08:41:34 -04:00
Shane McDonald
460c7c3379 Allow for dynamically scaling automation mesh in dev env 2021-08-24 08:41:32 -04:00
Alan Rominger
9881bb72b8 Treat the awx_1 node as a hybrid node for now, use local work type (#10726) 2021-08-24 08:40:21 -04:00
beeankha
264c560a8a Template docker receptor yaml file, update Makefile to reflect this change 2021-08-24 08:40:21 -04:00
beeankha
2fc581c249 Pull in user's uid vs hardcode to 1000 2021-08-24 08:40:20 -04:00
Jim Ladd
a79d7444e5 set userid to 1000 (#10714) 2021-08-24 08:40:20 -04:00
beeankha
f8d074db01 Point to correct config file for execution_node_1 2021-08-24 08:40:20 -04:00
Bianca Henderson
c3843004aa Update docker-compose (#10664)
* Update docker-compose

- Deploys 1 control and 1 execution node

* Add a new Receptor cluster configuration file

* update receptor peer to awx_1
to match how hop node is configured in cluster (Jim Ladd's commit)

* Move receptor_1 instantiation in the docker-compose setup

* Hard code receptor_1 name

* Update execution node name, move standalone conf file to docker-compose directory

* Reformat docker-compose file, mount another volume, change privileges
2021-08-24 08:40:20 -04:00
Alan Rominger
f597205fa7 Run capacity checks with container isolation (#10688)
This requires swapping out the container images
  for the execution nodes from awx-ee to the awx image

For completeness, the hop node image is switched to the raw
  receptor image

A few outright bugs are fixed here
  memory calculation just was not right at all
  the execution_capacity calculation was reverse of intention

Drop in a few TODOs about error handling from debugging
2021-08-24 08:40:19 -04:00
Alan Rominger
e7be86867d Fix rebase bug specific to ad hoc commands 2021-08-24 08:40:19 -04:00
Alan Rominger
13300bdbd4 Update rebase to keep old control plane capacity check
Also do some basic work to separate control versus execution capacity
  this is to assure that we don't send jobs to the control node
2021-08-24 08:40:19 -04:00
Alan Rominger
b09da48835 Remove some diff that we dont want from PoC 2021-08-24 08:40:19 -04:00
Alan Rominger
39e23db523 Make minor changes to add needed imports 2021-08-24 08:40:19 -04:00
Alan Rominger
b10a8b0fa9 Initial functionality tweaks 2021-08-24 08:40:18 -04:00
Ryan Petrello
05cb876df5 implement an initial development environment for receptor-based clusters 2021-08-24 08:40:18 -04:00
Kersom
4a271d6897 Merge pull request #10928 from mabashian/deps-audit-autofix
Auto fix dep audit
2021-08-24 08:34:14 -04:00
mabashian
41342883d4 Auto fix dep audit 2021-08-23 16:12:54 -04:00
Kia Lam
cc7488bc15 Disable checkbox for instances with node type control. 2021-08-23 15:06:36 -04:00
Jake McDermott
367e0a5e87 Merge pull request #10917 from AlexSCorey/10223-InventorySourceData
Adds source data to job list and job details view
2021-08-20 15:44:17 -04:00
Kersom
4a2917b6a0 Merge pull request #10859 from nixocio/ui_issue_warning_session
Remove warning for SSO session when logging in
2021-08-20 14:23:30 -04:00
kialam
c6a63d01db Merge pull request #10898 from kialam/fix-10718-null-datetime
Validate that start/end datetime creates at least 1 schedule.
2021-08-20 13:42:53 -04:00
nixocio
0694cb9a7d Remove warning for SSO session when logging in
Remove warning for SSO session when logging in

See: https://github.com/ansible/awx/issues/10860
2021-08-20 11:25:10 -04:00
Kia Lam
da2bf4c510 Validate that start/end datetime creates at least 1 schedule. 2021-08-19 18:39:05 -04:00
Alex Corey
48a044cc68 Adds source data to job list and job details view 2021-08-19 14:11:44 -04:00
Shane McDonald
b7c0f02cb1 Merge pull request #10915 from rooftopcellist/fix-make-target
Add new COMPOSE_UP_PRE_OPTS variable to docker-compose up target
2021-08-19 13:04:23 -04:00
Christian M. Adams
a76194c493 Add new COMPOSE_OPTS variable to docker-compose up target
Signed-off-by: Christian M. Adams <chadams@redhat.com>
2021-08-19 12:43:16 -04:00
Alex Corey
86390152bc Merge pull request #10907 from AlexSCorey/10872-ApprovalNodeValidation
properly validates node
2021-08-19 09:09:02 -04:00
Alexander Komarov
899d36b2c9 Fix tests 2021-08-19 15:20:52 +05:00
Alexander Komarov
530977d6b3 Set default value is 0 for idle_timeout 2021-08-19 15:18:38 +05:00
Alexander Komarov
aa682fa2c9 Add idle_timeout setting to job settings 2021-08-19 14:48:29 +05:00
Alex Corey
28ad404baa properly validates node 2021-08-18 15:46:50 -04:00
Tiago Góes
1ff8ebab94 Merge pull request #10892 from guyomog78/patch-1
fix typo Bienvenue French logon  screen
2021-08-18 14:57:41 -03:00
Bianca Henderson
c616678beb Merge pull request #10903 from beeankha/misc_collections_updates
Fix Test Playbooks, Update README, Make Module Docs More Informative
2021-08-18 12:41:38 -04:00
Keith J. Grant
500d407099 delete duplicate prop 2021-08-18 09:17:41 -07:00
Keith J. Grant
b99129c6b2 stub notifications api in test 2021-08-18 09:17:40 -07:00
Keith J. Grant
60f1919791 update searchableKeys in all Lookups & JobOutput 2021-08-18 09:17:40 -07:00
Keith J. Grant
262a2b70e2 update all lists to use getSearchableKeys helper 2021-08-18 09:17:40 -07:00
Keith J. Grant
977164b920 cleanup tests/advanced search changes 2021-08-18 09:12:21 -07:00
Keith J. Grant
a0df379225 limit advanced search options by field type 2021-08-18 09:12:21 -07:00
Keith J. Grant
b5bc9bb3f4 JobOutput: extract multiple helper functions 2021-08-18 08:54:35 -07:00
Keith J. Grant
b5708a8cc4 Revert "JobOutput: extract JobOutputPane"
This reverts commit 903de92969bf931cf0c01eb2fbfb703842c5ea83.
2021-08-18 08:54:35 -07:00
Keith J. Grant
c8604c73a9 JobOutput: extract JobOutputPane 2021-08-18 08:54:35 -07:00
Keith J. Grant
949c2b92af JobOutput: extract helper funcs into separate file 2021-08-18 08:54:35 -07:00
Keith J. Grant
5473e54219 JobOutput: extract JobOutputSearch bar 2021-08-18 08:54:35 -07:00
Shane McDonald
aefc28a0ed Update README.md 2021-08-18 09:45:00 -04:00
Shane McDonald
f102b0ccf9 Add support for running CI checks directly on devel branch 2021-08-18 09:31:39 -04:00
Shane McDonald
55e37f6229 Update ci.yml 2021-08-18 09:07:30 -04:00
beeankha
ad0dc028f2 Update README with recent Collections changes 2021-08-18 09:04:09 -04:00
Jim Ladd
e3893b1887 do not collect artifact_data when gathering analytics
- also, store event_data in jsonb object
- .. in order to have data structure that supports '-' operator
2021-08-17 14:55:16 -07:00
beeankha
c89296e76d Update integration test playbooks to work with most current Collections modules 2021-08-17 13:50:35 -04:00
Tiago Góes
c58fef949d Merge pull request #10849 from nixocio/ui_issue_10775_again
Update useBrandName
2021-08-16 18:44:16 -03:00
Tiago
26ab6dd264 Fix broken test 2021-08-16 18:29:59 -03:00
Tiago Góes
abf870e604 Merge pull request #10885 from gruselglatz/patch-1
Update Readme.md
2021-08-16 16:58:32 -03:00
guyomog78
a83aa7c0ae fix typo Bienvenu French logon screen 2021-08-16 19:08:19 +02:00
kialam
82fe099060 Merge pull request #10783 from kialam/fix-10587-managed-job-time
Fix 10587 managed job time
2021-08-16 12:18:34 -04:00
Kia Lam
304ec80d80 Convert dates to use luxon.js 2021-08-16 08:31:30 -07:00
Bianca Henderson
f6104dd438 Merge pull request #10888 from beeankha/update_collection_runtime_file
Update runtime.yml to Include Ansible Version Requirement
2021-08-16 10:36:46 -04:00
beeankha
7fadc00fb3 Update runtime.yml to include ansible version requirement 2021-08-16 09:23:06 -04:00
herbert
26e5830b80 Update Readme.md
fix path to inventory file
2021-08-16 13:21:19 +02:00
nixocio
efcac6d55a Update useBrandName
Update useBrandName and its usage.

It was verified on downstrean this solution.

See:https://github.com/ansible/awx/issues/10775
2021-08-12 17:05:35 -04:00
620 changed files with 35557 additions and 26421 deletions

View File

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

View File

@@ -1,3 +1,11 @@
<!--- 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 -->

View File

@@ -1,5 +1,7 @@
---
name: CI
env:
BRANCH: ${{ github.base_ref || 'devel' }}
on:
pull_request:
jobs:
@@ -17,16 +19,16 @@ jobs:
- name: Pre-pull image to warm build cache
run: |
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }}
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} || :
- name: Build image
run: |
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ github.base_ref }} make docker-compose-build
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ env.BRANCH }} make docker-compose-build
- name: Run API Tests
run: |
docker run -u $(id -u) --rm -v ${{ github.workspace}}:/awx_devel/:Z \
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }} /start_tests.sh
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} /start_tests.sh
api-lint:
runs-on: ubuntu-latest
permissions:
@@ -41,16 +43,16 @@ jobs:
- name: Pre-pull image to warm build cache
run: |
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }}
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} || :
- name: Build image
run: |
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ github.base_ref }} make docker-compose-build
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ env.BRANCH }} make docker-compose-build
- name: Run API Linters
run: |
docker run -u $(id -u) --rm -v ${{ github.workspace}}:/awx_devel/:Z \
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }} /var/lib/awx/venv/awx/bin/tox -e linters
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} /var/lib/awx/venv/awx/bin/tox -e linters
api-swagger:
runs-on: ubuntu-latest
permissions:
@@ -65,16 +67,16 @@ jobs:
- name: Pre-pull image to warm build cache
run: |
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }} || :
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} || : || :
- name: Build image
run: |
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ github.base_ref }} make docker-compose-build
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ env.BRANCH }} make docker-compose-build
- name: Generate API Reference
run: |
docker run -u $(id -u) --rm -v ${{ github.workspace}}:/awx_devel/:Z \
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }} /start_tests.sh swagger
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} /start_tests.sh swagger
awx-collection:
runs-on: ubuntu-latest
permissions:
@@ -89,16 +91,16 @@ jobs:
- name: Pre-pull image to warm build cache
run: |
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }}
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} || :
- name: Build image
run: |
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ github.base_ref }} make docker-compose-build
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ env.BRANCH }} make docker-compose-build
- name: Run Collection Tests
run: |
docker run -u $(id -u) --rm -v ${{ github.workspace}}:/awx_devel/:Z \
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }} /start_tests.sh test_collection_all
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} /start_tests.sh test_collection_all
api-schema:
runs-on: ubuntu-latest
permissions:
@@ -113,16 +115,16 @@ jobs:
- name: Pre-pull image to warm build cache
run: |
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }}
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} || :
- name: Build image
run: |
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ github.base_ref }} make docker-compose-build
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ env.BRANCH }} make docker-compose-build
- name: Check API Schema
run: |
docker run -u $(id -u) --rm -v ${{ github.workspace}}:/awx_devel/:Z \
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }} /start_tests.sh detect-schema-change
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} /start_tests.sh detect-schema-change
ui-lint:
runs-on: ubuntu-latest
permissions:
@@ -137,16 +139,16 @@ jobs:
- name: Pre-pull image to warm build cache
run: |
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }}
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} || :
- name: Build image
run: |
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ github.base_ref }} make docker-compose-build
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ env.BRANCH }} make docker-compose-build
- name: Run UI Linters
run: |
docker run -u $(id -u) --rm -v ${{ github.workspace}}:/awx_devel/:Z \
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }} make ui-lint
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} make ui-lint
ui-test:
runs-on: ubuntu-latest
permissions:
@@ -161,13 +163,51 @@ jobs:
- name: Pre-pull image to warm build cache
run: |
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }}
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} || :
- name: Build image
run: |
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ github.base_ref }} make docker-compose-build
DEV_DOCKER_TAG_BASE=ghcr.io/${{ github.repository_owner }} COMPOSE_TAG=${{ env.BRANCH }} make docker-compose-build
- name: Run UI Tests
run: |
docker run -u $(id -u) --rm -v ${{ github.workspace}}:/awx_devel/:Z \
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ github.base_ref }} make ui-test
--workdir=/awx_devel ghcr.io/${{ github.repository_owner }}/awx_devel:${{ env.BRANCH }} make ui-test
awx-operator:
runs-on: ubuntu-latest
steps:
- name: Checkout awx
uses: actions/checkout@v2
with:
path: awx
- name: Checkout awx-operator
uses: actions/checkout@v2
with:
repository: ansible/awx-operator
path: awx-operator
- name: Install playbook dependencies
run: |
python3 -m pip install docker
- name: Build AWX image
working-directory: awx
run: |
ansible-playbook -v tools/ansible/build.yml \
-e headless=yes \
-e awx_image=awx \
-e awx_image_tag=ci \
-e ansible_python_interpreter=$(which python3)
- name: Run test deployment with awx-operator
working-directory: awx-operator
run: |
python3 -m pip install -r molecule/requirements.txt
ansible-galaxy collection install -r molecule/requirements.yml
sudo rm -f $(which kustomize)
make kustomize
KUSTOMIZE_PATH=$(readlink -f bin/kustomize) molecule test -s kind
env:
AWX_TEST_IMAGE: awx
AWX_TEST_VERSION: ci

View File

@@ -85,7 +85,7 @@ jobs:
-e CYPRESS_baseUrl="https://$AWX_IP:8043" \
-e CYPRESS_AWX_E2E_USERNAME=admin \
-e CYPRESS_AWX_E2E_PASSWORD='password' \
-e COMMAND="npm run cypress-gha" \
-e COMMAND="npm run cypress-concurrently-gha" \
-v /dev/shm:/dev/shm \
-v $PWD:/e2e \
-w /e2e \

26
.github/workflows/promote.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
---
name: Promote Release
on:
release:
types: [published]
jobs:
promote:
runs-on: ubuntu-latest
steps:
- name: Log in to GHCR
run: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Log in to Quay
run: |
echo ${{ secrets.QUAY_TOKEN }} | docker login quay.io -u ${{ secrets.QUAY_USER }} --password-stdin
- name: Re-tag and promote awx image
run: |
docker pull ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }}
docker tag ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} quay.io/${{ github.repository }}:${{ github.event.release.tag_name }}
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

123
.github/workflows/stage.yml vendored Normal file
View File

@@ -0,0 +1,123 @@
---
name: Stage Release
on:
workflow_dispatch:
inputs:
version:
description: 'AWX version.'
required: true
default: ''
operator_version:
description: 'Operator version. Leave blank to skip staging awx-operator.'
default: ''
confirm:
description: 'Are you sure? Set this to yes.'
required: true
default: 'no'
jobs:
stage:
runs-on: ubuntu-latest
permissions:
packages: write
contents: write
steps:
- name: Verify inputs
run: |
set -e
if [[ ${{ github.event.inputs.confirm }} != "yes" ]]; then
>&2 echo "Confirm must be 'yes'"
exit 1
fi
if [[ ${{ github.event.inputs.version }} == "" ]]; then
>&2 echo "Set version to continue."
exit 1
fi
exit 0
- name: Checkout awx
uses: actions/checkout@v2
with:
path: awx
- name: Checkout awx-logos
uses: actions/checkout@v2
with:
repository: ansible/awx-logos
path: awx-logos
- name: Checkout awx-operator
uses: actions/checkout@v2
with:
repository: ${{ github.repository_owner }}/awx-operator
path: awx-operator
- name: Install playbook dependencies
run: |
python3 -m pip install docker
- name: Build and stage AWX
working-directory: awx
run: |
ansible-playbook -v tools/ansible/build.yml \
-e registry=ghcr.io \
-e registry_username=${{ github.actor }} \
-e registry_password=${{ secrets.GITHUB_TOKEN }} \
-e awx_image=${{ github.repository }} \
-e awx_version=${{ github.event.inputs.version }} \
-e ansible_python_interpreter=$(which python3) \
-e push=yes \
-e awx_official=yes
- name: Build and stage awx-operator
working-directory: awx-operator
run: |
BUILD_ARGS="--build-arg DEFAULT_AWX_VERSION=${{ github.event.inputs.version }}" \
IMAGE_TAG_BASE=ghcr.io/${{ github.repository_owner }}/awx-operator \
VERSION=${{ github.event.inputs.operator_version }} make docker-build docker-push
- name: Run test deployment with awx-operator
working-directory: awx-operator
run: |
python3 -m pip install -r molecule/requirements.txt
ansible-galaxy collection install -r molecule/requirements.yml
sudo rm -f $(which kustomize)
make kustomize
KUSTOMIZE_PATH=$(readlink -f bin/kustomize) molecule test -s kind
env:
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 }} \
-e github_token=${{ secrets.GITHUB_TOKEN }}
- name: Create draft release for awx-operator
if: ${{ github.event.inputs.operator_version != '' }}
working-directory: awx
run: |
ansible-playbook tools/ansible/stage.yml \
-e version=${{ github.event.inputs.operator_version }} \
-e repo=${{ github.repository_owner }}/awx-operator \
-e github_token=${{ secrets.AWX_OPERATOR_RELEASE_TOKEN }}

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- devel
- release_4.1
jobs:
push:
runs-on: ubuntu-latest
@@ -19,7 +20,7 @@ jobs:
- name: Pre-pull image to warm build cache
run: |
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${GITHUB_REF##*/}
docker pull ghcr.io/${{ github.repository_owner }}/awx_devel:${GITHUB_REF##*/} || :
- name: Build image
run: |

2
.gitignore vendored
View File

@@ -42,6 +42,7 @@ tools/docker-compose/_build
tools/docker-compose/_sources
tools/docker-compose/overrides/
tools/docker-compose-minikube/_sources
tools/docker-compose/keycloak.awx.realm.json
# Tower setup playbook testing
setup/test/roles/postgresql
@@ -58,6 +59,7 @@ __pycache__
/dist
/*.egg-info
*.py[c,o]
/.eggs
# JavaScript
/Gruntfile.js

View File

@@ -6,8 +6,11 @@ ignore: |
# vault files
awx/main/tests/data/ansible_utils/playbooks/valid/vault.yml
awx/ui/test/e2e/tests/smoke-vars.yml
awx/ui/node_modules
tools/docker-compose/_sources
extends: default
rules:
line-length: disable
truthy: disable

View File

@@ -1,520 +1,7 @@
# Changelog
# 19.3.0 (August 12, 2021)
**Note:** This file is deprecated and will be removed at some point in a future release.
- Fixed threading bug that would sometimes cause jobs to randomly fail (https://github.com/ansible/awx/pull/10537)
- Fixed race where app would crash when postgres is not available (https://github.com/ansible/awx/pull/10583)
- Add support for workflow node aliasing via identifier field (https://github.com/ansible/awx/pull/10592)
- Add UI support for management jobs in workflows (https://github.com/ansible/awx/pull/10572)
- Show PAT as part of bulk delete list (https://github.com/ansible/awx/pull/10794)
- Return 404 for ad_hoc_command_events list api. Remove api endtpoint (https://github.com/ansible/awx/pull/10716)
- Fix multiple accessibility violations (https://github.com/ansible/awx/pull/10713)
- Fix ignoring --no-color for awx-manage list_instances command (https://github.com/ansible/awx/pull/10668)
- Fix to handle ask_* parameters correctly when set false (https://github.com/ansible/awx/pull/10108)
- Default source_project to organization for inventory source (https://github.com/ansible/awx/pull/10573)
- Fix headers missing in webhook notification request (https://github.com/ansible/awx/pull/10566)
- Avoid double LDAP updates (https://github.com/ansible/awx/pull/9703)
- introduced a pre-flight check for postgres 12 (https://github.com/ansible/awx/pull/10425)
- Fix Job Settings Page Break on Firefox (https://github.com/ansible/awx/pull/10523)
- bumped django version to 2.2.20 (https://github.com/ansible/awx/pull/10564)
- Add Thycotic SecretServer support (https://github.com/ansible/awx/pull/10632)
Starting with AWX 20, release notes are published to [GitHub Releases](https://github.com/ansible/awx/releases).
# 19.2.2 (June 28, 2021)
- Fixed bug where symlinks pointing to directories were not preserved (https://github.com/ansible/ansible-runner/pull/736)
- Various bugfixes found during testing (https://github.com/ansible/awx/pull/10532)
# 19.2.1 (June 17, 2021)
- There are now 2 default Instance Groups: 'controlplane' and 'default' (https://github.com/ansible/awx/pull/10324)
- Removed deprecated modules: `tower_send`, `tower_receive`, `tower_workflow_template` (https://github.com/ansible/awx/pull/9980)
- Improved UI performance when a large amount of events are being emitted by jobs (https://github.com/ansible/awx/pull/10053)
- Settings UI Revert All button now issues a DELETE instead of PATCHing all fields (https://github.com/ansible/awx/pull/10376)
- Fixed a bug with the schedule date/time picker in Firefox (https://github.com/ansible/awx/pull/10291)
- UI now preselects the system default Galaxy credential when creating a new organization (https://github.com/ansible/awx/pull/10395)
- Added favicon (https://github.com/ansible/awx/pull/10388)
- Removed `not` option from smart inventory host filter search as it's not supported by the API (https://github.com/ansible/awx/pull/10380)
- Added button to allow user to refetch project revision after project sync has finished (https://github.com/ansible/awx/pull/10334)
- Fixed bug where extraneous CONFIG requests were made on logout (https://github.com/ansible/awx/pull/10379)
- Fixed bug where users were unable to cancel inventory syncs (https://github.com/ansible/awx/pull/10346)
- Added missing dashboard graph filters (https://github.com/ansible/awx/pull/10349)
- Added support for typing in to single select lookup form fields (https://github.com/ansible/awx/pull/10257)
- Fixed various bugs related to user sessions (https://github.com/ansible/awx/pull/9908)
- Fixed bug where sorting in modals would close the modal (https://github.com/ansible/awx/pull/10215)
- Added support for Red Hat Insights as an inventory source (https://github.com/ansible/awx/pull/8650)
- Fixed bugs when selecting items in a list then sorting/paginating (https://github.com/ansible/awx/pull/10329)
# 19.2.0 (June 1, 2021)
- Fixed race condition that would sometimes cause jobs to error out at the very end of an otherwise successful run (https://github.com/ansible/receptor/pull/328)
- Fixes bug where users were unable to click on text next to checkboxes in modals (https://github.com/ansible/awx/pull/10279)
- Have the project update playbook warn if role/collection syncing is disabled. (https://github.com/ansible/awx/pull/10068)
- Move irc references to point to irc.libera.chat (https://github.com/ansible/awx/pull/10295)
- Fixes bug where activity stream changes were displaying as [object object] (https://github.com/ansible/awx/pull/10267)
- Update awxkit to enable export of Galaxy credentials associated to organizations (https://github.com/ansible/awx/pull/10271)
- Bump receptor and receptorctl versions to 1.0.0a2 (https://github.com/ansible/awx/pull/10261)
- Add the ability to disable local authentication (https://github.com/ansible/awx/pull/10102)
- Show error if no Execution Environment is found on project sync/job run (https://github.com/ansible/awx/pull/10183)
- Allow for editing and deleting managed_by_tower EEs from API/UI (https://github.com/ansible/awx/pull/10173)
# 19.1.0 (May 1, 2021)
- Custom inventory scripts have been removed from the API https://github.com/ansible/awx/pull/9822
- Old scripts can be exported via `awx-manage export_custom_scripts`
- Fixed a bug where ad-hoc commands targeted against multiple hosts would run against only 1 host https://github.com/ansible/awx/pull/9973
- AWX will now look for a top-level requirements.yml when installing collections / roles in project updates https://github.com/ansible/awx/pull/9945
- Improved error handling when Container Group pods fail to launch https://github.com/ansible/awx/pull/10025
- Added ability to set server-side password policies using Django's AUTH_PASSWORD_VALIDATORS setting https://github.com/ansible/awx/pull/9999
- Bumped versions of Ansible Runner & AWX EE https://github.com/ansible/awx/pull/10013
- If you have built any custom EEs on top of awx-ee 0.1.0, you will need to rebuild on top of 0.2.0.
- Remove legacy resource profiling code https://github.com/ansible/awx/pull/9883
# 19.0.0 (April 7, 2021)
- AWX now runs on Python 3.8 (https://github.com/ansible/awx/pull/8778/)
- Fixed inventories-from-projects when running in Kubernetes (https://github.com/ansible/awx/pull/9741)
- Fixed a bug where a slash was appended to invetory file paths in UI dropdown (https://github.com/ansible/awx/pull/9713)
- Fix a bug with large file parsing in project sync (https://github.com/ansible/awx/pull/9627)
- Fix k8s credentials that use a custom ca cert (https://github.com/ansible/awx/pull/9744)
- Fix a bug that allowed a user to attempt deleting a running job (https://github.com/ansible/awx/pull/9758)
- Fixed the Kubernetes Pod reaper to properly delete Pods launched by Receptor (https://github.com/ansible/awx/pull/9819)
- AWX Collection Modules: added ability to set instance groups for organization, job templates, and inventories. (https://github.com/ansible/awx/pull/9804)
- Fixed CSP violation errors on job details and job settings views (https://github.com/ansible/awx/pull/9818)
- Added support for convergence any/all on workflow nodes (https://github.com/ansible/awx/pull/9737)
- Fixed race condition that causes InvalidGitRepositoryError (https://github.com/ansible/awx/pull/9754)
- Added support for Execution Environments to the Activity Stream (https://github.com/ansible/awx/issues/9308)
- Fixed a bug that improperly formats OpenSSH keys specified in custom Credential Types (https://github.com/ansible/awx/issues/9361)
- Fixed an HTTP 500 error for unauthenticated users (https://github.com/ansible/awx/pull/9725)
- Added subscription wizard: https://github.com/ansible/awx/pull/9496
# 18.0.0 (March 23, 2021)
**IMPORTANT INSTALL AND UPGRADE NOTES**
Starting in version 18.0, the [AWX Operator](https://github.com/ansible/awx-operator) is the preferred way to install AWX: https://github.com/ansible/awx/blob/devel/INSTALL.md#installing-awx
If you have a pre-existing installation of AWX that utilizes the Docker-based installation method, this install method has ** notably changed** from 17.x to 18.x. For details, please see:
- https://groups.google.com/g/awx-project/c/47MjWSUQaOc/m/bCjSDn0eBQAJ
- https://github.com/ansible/awx/blob/devel/tools/docker-compose
- https://github.com/ansible/awx/blob/devel/tools/docker-compose/docs/data_migration.md
### Introducing Execution Environments
After a herculean effort from a number of contributors, we're excited to announce that AWX 18.0.0 introduces a new concept called Execution Environments.
Execution Environments are container images which consist of everything necessary to run a playbook within AWX, and which drive the entire management and lifecycle of playbook execution runtime in AWX: https://github.com/ansible/awx/issues/5157. This means that going forward, AWX no longer utilizes the [bubblewrap](https://github.com/containers/bubblewrap) project for playbook isolation, but instead utilizes a container per playbook run.
Much like custom virtualenvs, custom Execution Environments can be crafted to specify additional Python or system-level dependencies. [Ansible Builder](https://github.com/ansible/ansible-builder) outputs images you can upload to your registry which can *then* be defined in AWX and utilized for playbook runs.
To learn more about Ansible Builder and Execution Environments, see: https://www.ansible.com/blog/introduction-to-ansible-builder
### Other Notable Changes
- Removed `installer` directory.
- The Kubernetes installer has been removed in favor of [AWX Operator](https://github.com/ansible/awx-operator). Official images for Operator-based installs are no longer hosted on Docker Hub, but are instead available on [Quay](https://quay.io/repository/ansible/awx?tab=tags).
- The "Local Docker" install method has been removed in favor of the development environment. Details can be found at: https://github.com/ansible/awx/blob/devel/tools/docker-compose/README.md
- Removal of custom virtual environments https://github.com/ansible/awx/pull/9498
- Custom virtual environments have been replaced by Execution Environments https://github.com/ansible/awx/pull/9570
- The default Container Group Pod definition has changed. All custom Pod specs have been reset. https://github.com/ansible/awx/commit/05ef51f710dad8f8036bc5acee4097db4adc0d71
- Added user interface for the activity stream: https://github.com/ansible/awx/pull/9083
- Converted many of the top-level list views (Jobs, Teams, Hosts, Inventories, Projects, and more) to a new, permanent table component for substantially increased responsiveness, usability, maintainability, and other 'ility's: https://github.com/ansible/awx/pull/8970, https://github.com/ansible/awx/pull/9182 and many others!
- Added support for Centrify Vault (https://www.centrify.com) as a credential lookup plugin (https://github.com/ansible/awx/pull/9542)
- Added support for namespaces in Hashicorp Vault credential plugin (https://github.com/ansible/awx/pull/9590)
- Added click-to-expand details for job tables
- Added search filtering to job output https://github.com/ansible/awx/pull/9208
- Added the new migration, update, and "installation in progress" page https://github.com/ansible/awx/pull/9123
- Added the user interface for job settings https://github.com/ansible/awx/pull/8661
- Runtime errors from jobs are now displayed, along with an explanation for what went wrong, on the output page https://github.com/ansible/awx/pull/8726
- You can now cancel a running job from its output and details panel https://github.com/ansible/awx/pull/9199
- Fixed a bug where launch prompt inputs were unexpectedly deposited in the url: https://github.com/ansible/awx/pull/9231
- Playbook, credential type, and inventory file inputs now support type-ahead and manual type-in! https://github.com/ansible/awx/pull/9120
- Added ability to relaunch against failed hosts: https://github.com/ansible/awx/pull/9225
- Added pending workflow approval count to the application header https://github.com/ansible/awx/pull/9334
- Added user interface for management jobs: https://github.com/ansible/awx/pull/9224
- Added toast message to show notification template test result to notification templates list https://github.com/ansible/awx/pull/9318
- Replaced CodeMirror with AceEditor for editing template variables and notification templates https://github.com/ansible/awx/pull/9281
- Added support for filtering and pagination on job output https://github.com/ansible/awx/pull/9208
- Added support for html in custom login text https://github.com/ansible/awx/pull/9519
# 17.1.0 (March 9, 2021)
- Addressed a security issue in AWX (CVE-2021-20253)
- Fixed a bug permissions error related to redis in K8S-based deployments: https://github.com/ansible/awx/issues/9401
# 17.0.1 (January 26, 2021)
- Fixed pgdocker directory permissions issue with Local Docker installer: https://github.com/ansible/awx/pull/9152
- Fixed a bug in the UI which caused toggle settings to not be changed when clicked: https://github.com/ansible/awx/pull/9093
# 17.0.0 (January 22, 2021)
- AWX now requires PostgreSQL 12 by default: https://github.com/ansible/awx/pull/8943
**Note:** users who encounter permissions errors at upgrade time should `chown -R ~/.awx/pgdocker` to ensure it's owned by the user running the install playbook
- Added support for region name for OpenStack inventory: https://github.com/ansible/awx/issues/5080
- Added the ability to chain undefined attributes in custom notification templates: https://github.com/ansible/awx/issues/8677
- Dramatically simplified the `image_build` role: https://github.com/ansible/awx/pull/8980
- Fixed a bug which can cause schema migrations to fail at install time: https://github.com/ansible/awx/issues/9077
- Fixed a bug which caused the `is_superuser` user property to be out of date in certain circumstances: https://github.com/ansible/awx/pull/8833
- Fixed a bug which sometimes results in race conditions on setting access: https://github.com/ansible/awx/pull/8580
- Fixed a bug which sometimes causes an unexpected delay in stdout for some playbooks: https://github.com/ansible/awx/issues/9085
- (UI) Added support for credential password prompting on job launch: https://github.com/ansible/awx/pull/9028
- (UI) Added the ability to configure LDAP settings in the UI: https://github.com/ansible/awx/issues/8291
- (UI) Added a sync button to the Project detail view: https://github.com/ansible/awx/issues/8847
- (UI) Added a form for configuring Google Outh 2.0 settings: https://github.com/ansible/awx/pull/8762
- (UI) Added searchable keys and related keys to the Credentials list: https://github.com/ansible/awx/issues/8603
- (UI) Added support for advanced search and copying to Notification Templates: https://github.com/ansible/awx/issues/7879
- (UI) Added support for prompting on workflow nodes: https://github.com/ansible/awx/issues/5913
- (UI) Added support for session timeouts: https://github.com/ansible/awx/pull/8250
- (UI) Fixed a bug that broke websocket streaming for the insecure ws:// protocol: https://github.com/ansible/awx/pull/8877
- (UI) Fixed a bug in the user interface when a translation for the browser's preferred locale isn't available: https://github.com/ansible/awx/issues/8884
- (UI) Fixed bug where navigating from one survey question form directly to another wasn't reloading the form: https://github.com/ansible/awx/issues/7522
- (UI) Fixed a bug which can cause an uncaught error while launching a Job Template: https://github.com/ansible/awx/issues/8936
- Updated autobahn to address CVE-2020-35678
## 16.0.0 (December 10, 2020)
- AWX now ships with a reimagined user interface. **Please read this before upgrading:** https://groups.google.com/g/awx-project/c/KuT5Ao92HWo
- Removed support for syncing inventory from Red Hat CloudForms - https://github.com/ansible/awx/commit/0b701b3b2
- Removed support for Mercurial-based project updates - https://github.com/ansible/awx/issues/7932
- Upgraded NodeJS to actively maintained LTS 14.15.1 - https://github.com/ansible/awx/pull/8766
- Added Git-LFS to the default image build - https://github.com/ansible/awx/pull/8700
- Added the ability to specify `metadata.labels` in the podspec for container groups - https://github.com/ansible/awx/issues/8486
- Added support for Kubernetes pod annotations - https://github.com/ansible/awx/pull/8434
- Added the ability to label the web container in local Docker installs - https://github.com/ansible/awx/pull/8449
- Added additional metadata (as an extra var) to playbook runs to report the SCM branch name - https://github.com/ansible/awx/pull/8433
- Fixed a bug that caused k8s installations to fail due to an incorrect Helm repo - https://github.com/ansible/awx/issues/8715
- Fixed a bug that prevented certain Workflow Approval resources from being deleted - https://github.com/ansible/awx/pull/8612
- Fixed a bug that prevented the deletion of inventories stuck in "pending deletion" state - https://github.com/ansible/awx/issues/8525
- Fixed a display bug in webhook notifications with certain unicode characters - https://github.com/ansible/awx/issues/7400
- Improved support for exporting dependent objects (Inventory Hosts and Groups) in the `awx export` CLI tool - https://github.com/ansible/awx/commit/607bc0788
## 15.0.1 (October 20, 2020)
- Added several optimizations to improve performance for a variety of high-load simultaneous job launch use cases https://github.com/ansible/awx/pull/8403
- Added the ability to source roles and collections from requirements.yaml files (not just requirements.yml) - https://github.com/ansible/awx/issues/4540
- awx.awx collection modules now provide a clearer error message for incompatible versions of awxkit - https://github.com/ansible/awx/issues/8127
- Fixed a bug in notification messages that contain certain unicode characters - https://github.com/ansible/awx/issues/7400
- Fixed a bug that prevents the deletion of Workflow Approval records - https://github.com/ansible/awx/issues/8305
- Fixed a bug that broke the selection of webhook credentials - https://github.com/ansible/awx/issues/7892
- Fixed a bug which can cause confusing behavior for social auth logins across distinct browser tabs - https://github.com/ansible/awx/issues/8154
- Fixed several bugs in the output of Workflow Job Templates using the `awx export` tool - https://github.com/ansible/awx/issues/7798 https://github.com/ansible/awx/pull/7847
- Fixed a race condition that can lead to missing hosts when running parallel inventory syncs - https://github.com/ansible/awx/issues/5571
- Fixed an HTTP 500 error when certain LDAP group parameters aren't properly set - https://github.com/ansible/awx/issues/7622
- Updated a few dependencies in response to several CVEs:
* CVE-2020-7720
* CVE-2020-7743
* CVE-2020-7676
## 15.0.0 (September 30, 2020)
- Added improved support for fetching Ansible collections from private Galaxy content sources (such as https://github.com/ansible/galaxy_ng) - https://github.com/ansible/awx/issues/7813
**Note:** as part of this change, new Organizations created in the AWX API will _no longer_ automatically synchronize roles and collections from galaxy.ansible.com by default. More details on this change can be found at: https://github.com/ansible/awx/issues/8341#issuecomment-707310633
- AWX now utilizes a version of certifi that auto-discovers certificates in the system certificate store - https://github.com/ansible/awx/pull/8242
- Added support for arbitrary custom inventory plugin configuration: https://github.com/ansible/awx/issues/5150
- Added an optional setting to disable the auto-creation of organizations and teams on successful SAML login. - https://github.com/ansible/awx/pull/8069
- Added a number of optimizations to AWX's callback receiver to improve the speed of stdout processing for simultaneous playbooks runs - https://github.com/ansible/awx/pull/8193 https://github.com/ansible/awx/pull/8191
- Added the ability to use `!include` and `!import` constructors when constructing YAML for use with the AWX CLI - https://github.com/ansible/awx/issues/8135
- Fixed a bug that prevented certain users from being able to edit approval nodes in Workflows - https://github.com/ansible/awx/pull/8253
- Fixed a bug that broke password prompting for credentials in certain cases - https://github.com/ansible/awx/issues/8202
- Fixed a bug which can cause PostgreSQL deadlocks when running many parallel playbooks against large shared inventories - https://github.com/ansible/awx/issues/8145
- Fixed a bug which can cause delays in AWX's task manager when large numbers of simultaneous jobs are scheduled - https://github.com/ansible/awx/issues/7655
- Fixed a bug which can cause certain scheduled jobs - those that run every X minute(s) or hour(s) - to fail to run at the proper time - https://github.com/ansible/awx/issues/8071
- Fixed a performance issue for playbooks that store large amounts of data using the `set_stats` module - https://github.com/ansible/awx/issues/8006
- Fixed a bug related to AWX's handling of the auth_path argument for the HashiVault KeyValue credential plugin - https://github.com/ansible/awx/pull/7991
- Fixed a bug that broke support for Remote Archive SCM Type project syncs on platforms that utilize Python2 - https://github.com/ansible/awx/pull/8057
- Updated to the latest version of Django Rest Framework to address CVE-2020-25626
- Updated to the latest version of Django to address CVE-2020-24583 and CVE-2020-24584
- Updated to the latest verson of channels_redis to address a bug that slowly causes Daphne processes to leak memory over time - https://github.com/django/channels_redis/issues/212
## 14.1.0 (Aug 25, 2020)
- AWX images can now be built on ARM64 - https://github.com/ansible/awx/pull/7607
- Added the Remote Archive SCM Type to support using immutable artifacts and releases (such as tarballs and zip files) as projects - https://github.com/ansible/awx/issues/7954
- Deprecated official support for Mercurial-based project updates - https://github.com/ansible/awx/issues/7932
- Added resource import/export support to the official AWX collection - https://github.com/ansible/awx/issues/7329
- Added the ability to import YAML-based resources (instead of just JSON) when using the AWX CLI - https://github.com/ansible/awx/pull/7808
- Users upgrading from older versions of AWX may encounter an issue that causes their postgres container to restart in a loop (https://github.com/ansible/awx/issues/7854) - if you encounter this, bring your containers down and then back up (e.g., `docker-compose down && docker-compose up -d`) after upgrading to 14.1.0.
- Updated the AWX CLI to export labels associated with Workflow Job Templates - https://github.com/ansible/awx/pull/7847
- Updated to the latest python-ldap to address a bug - https://github.com/ansible/awx/issues/7868
- Upgraded git-python to fix a bug that caused workflows to sometimes fail - https://github.com/ansible/awx/issues/6119
- Worked around a bug in the channels_redis library that slowly causes Daphne processes to leak memory over time - https://github.com/django/channels_redis/issues/212
- Fixed a bug in the AWX CLI that prevented Workflow nodes from importing properly - https://github.com/ansible/awx/issues/7793
- Fixed a bug in the awx.awx collection release process that templated the wrong version - https://github.com/ansible/awx/issues/7870
- Fixed a bug that caused errors rendering stdout that contained UTF-16 surrogate pairs - https://github.com/ansible/awx/pull/7918
## 14.0.0 (Aug 6, 2020)
- As part of our commitment to inclusivity in open source, we recently took some time to audit AWX's source code and user interface and replace certain terminology with more inclusive language. Strictly speaking, this isn't a bug or a feature, but we think it's important and worth calling attention to:
* https://github.com/ansible/awx/commit/78229f58715fbfbf88177e54031f532543b57acc
* https://www.redhat.com/en/blog/making-open-source-more-inclusive-eradicating-problematic-language
- Installing roles and collections via requirements.yml as part of Project Updates now requires at least Ansible 2.9 - https://github.com/ansible/awx/issues/7769
- Deprecated the use of the `PRIMARY_GALAXY_USERNAME` and `PRIMARY_GALAXY_PASSWORD` settings. We recommend using tokens to access Galaxy or Automation Hub.
- Added local caching for downloaded roles and collections so they are not re-downloaded on nodes where they are up to date with the project - https://github.com/ansible/awx/issues/5518
- Added the ability to associate K8S/OpenShift credentials to Job Template for playbook interaction with the `community.kubernetes` collection - https://github.com/ansible/awx/issues/5735
- Added the ability to include HTML in the Custom Login Info presented on the login page - https://github.com/ansible/awx/issues/7600
- Fixed https://access.redhat.com/security/cve/cve-2020-14327 - Server-side request forgery on credentials
- Fixed https://access.redhat.com/security/cve/cve-2020-14328 - Server-side request forgery on webhooks
- Fixed https://access.redhat.com/security/cve/cve-2020-14329 - Sensitive data exposure on labels
- Fixed https://access.redhat.com/security/cve/cve-2020-14337 - Named URLs allow for testing the presence or absence of objects
- Fixed a number of bugs in the user interface related to an upgrade of jQuery:
* https://github.com/ansible/awx/issues/7530
* https://github.com/ansible/awx/issues/7546
* https://github.com/ansible/awx/issues/7534
* https://github.com/ansible/awx/issues/7606
- Fixed a bug that caused the `-f yaml` flag of the AWX CLI to not print properly formatted YAML - https://github.com/ansible/awx/issues/7795
- Fixed a bug in the installer that caused errors when `docker_registry_password` was set - https://github.com/ansible/awx/issues/7695
- Fixed a permissions error that prevented certain users from starting AWX services - https://github.com/ansible/awx/issues/7545
- Fixed a bug that allows superusers to run unsafe Jinja code when defining custom Credential Types - https://github.com/ansible/awx/pull/7584/
- Fixed a bug that prevented users from creating (or editing) custom Credential Types containing boolean fields - https://github.com/ansible/awx/issues/7483
- Fixed a bug that prevented users with postgres usernames containing uppercase letters from restoring backups succesfully - https://github.com/ansible/awx/pull/7519
- Fixed a bug which allowed the creation (in the Tower API) of Groups and Hosts with the same name - https://github.com/ansible/awx/issues/4680
## 13.0.0 (Jun 23, 2020)
- Added import and export commands to the official AWX CLI, replacing send and receive from the old tower-cli (https://github.com/ansible/awx/pull/6125).
- Removed scripts as a means of running inventory updates of built-in types (https://github.com/ansible/awx/pull/6911)
- Ansible 2.8 is now partially unsupported; some inventory source types are known to no longer work.
- Fixed an issue where the vmware inventory source ssl_verify source variable was not recognized (https://github.com/ansible/awx/pull/7360)
- Fixed a bug that caused redis' listen socket to have too-permissive file permissions (https://github.com/ansible/awx/pull/7317)
- Fixed a bug that caused rsyslogd's configuration file to have world-readable file permissions, potentially leaking secrets (CVE-2020-10782)
## 12.0.0 (Jun 9, 2020)
- Removed memcached as a dependency of AWX (https://github.com/ansible/awx/pull/7240)
- Moved to a single container image build instead of separate awx_web and awx_task images. The container image is just `awx` (https://github.com/ansible/awx/pull/7228)
- Official AWX container image builds now use a two-stage container build process that notably reduces the size of our published images (https://github.com/ansible/awx/pull/7017)
- Removed support for HipChat notifications ([EoL announcement](https://www.atlassian.com/partnerships/slack/faq#faq-98b17ca3-247f-423b-9a78-70a91681eff0)); all previously-created HipChat notification templates will be deleted due to this removal.
- Fixed a bug which broke AWX installations with oc version 4.3 (https://github.com/ansible/awx/pull/6948/)
- Fixed a performance issue that caused notable delay of stdout processing for playbooks run against large numbers of hosts (https://github.com/ansible/awx/issues/6991)
- Fixed a bug that caused CyberArk AIM credential plugin looks to hang forever in some environments (https://github.com/ansible/awx/issues/6986)
- Fixed a bug that caused ANY/ALL converage settings not to properly save when editing approval nodes in the UI (https://github.com/ansible/awx/issues/6998)
- Fixed a bug that broke support for the satellite6_group_prefix source variable (https://github.com/ansible/awx/issues/7031)
- Fixed a bug that prevented changes to workflow node convergence settings when approval nodes were in use (https://github.com/ansible/awx/issues/7063)
- Fixed a bug that caused notifications to fail on newer version of Mattermost (https://github.com/ansible/awx/issues/7264)
- Fixed a bug (by upgrading to 0.8.1 of the foreman collection) that prevented host_filters from working properly with Foreman-based inventory (https://github.com/ansible/awx/issues/7225)
- Fixed a bug that prevented the usage of the Conjur credential plugin with secrets that contain spaces (https://github.com/ansible/awx/issues/7191)
- Fixed a bug in awx-manage run_wsbroadcast --status in kubernetes (https://github.com/ansible/awx/pull/7009)
- Fixed a bug that broke notification toggles for system jobs in the UI (https://github.com/ansible/awx/pull/7042)
- Fixed a bug that broke local pip installs of awxkit (https://github.com/ansible/awx/issues/7107)
- Fixed a bug that prevented PagerDuty notifications from sending for workflow job template approvals (https://github.com/ansible/awx/issues/7094)
- Fixed a bug that broke external log aggregation support for URL paths that include the = character (such as the tokens for SumoLogic) (https://github.com/ansible/awx/issues/7139)
- Fixed a bug that prevented organization admins from removing labels from workflow job templates (https://github.com/ansible/awx/pull/7143)
## 11.2.0 (Apr 29, 2020)
- Inventory updates now use collection-based plugins by default (in Ansible 2.9+):
- amazon.aws.aws_ec2
- community.vmware.vmware_vm_inventory
- azure.azcollection.azure_rm
- google.cloud.gcp_compute
- theforeman.foreman.foreman
- openstack.cloud.openstack
- ovirt.ovirt_collection.ovirt
- awx.awx.tower
- Added support for Approle and LDAP/AD mechanisms to the Hashicorp Vault credential plugin (https://github.com/ansible/awx/issues/5076)
- Added Project (Domain Name) support for the OpenStack Keystone v3 API (https://github.com/ansible/awx/issues/6831)
- Added a new setting for raising log verbosity for rsyslogd (https://github.com/ansible/awx/pull/6818)
- Added the ability to monitor stdout in the CLI for running jobs and workflow jobs (https://github.com/ansible/awx/issues/6165)
- Fixed a bug which prevented the AWX CLI from properly installing with newer versions of pip (https://github.com/ansible/awx/issues/6870)
- Fixed a bug which broke AWX's external logging support when configured with HTTPS endpoints that utilize self-signed certificates (https://github.com/ansible/awx/issues/6851)
- Fixed a local docker installer bug that mistakenly attempted to upgrade PostgreSQL when an external pg_hostname is specified (https://github.com/ansible/awx/pull/5398)
- Fixed a race condition that caused task container crashes when pods are quickly brought down and back up (https://github.com/ansible/awx/issues/6750)
- Fixed a bug that caused 404 errors when attempting to view the second page of the workflow approvals view (https://github.com/ansible/awx/issues/6803)
- Fixed a bug that prevented the use of ANSIBLE_SSH_ARGS for ad-hoc-commands (https://github.com/ansible/awx/pull/6811)
- Fixed a bug that broke AWX installs/upgrades on Red Hat OpenShift (https://github.com/ansible/awx/issues/6791)
## 11.1.0 (Apr 22, 2020)
- Changed rsyslogd to persist queued events to disk (to prevent a risk of out-of-memory errors) (https://github.com/ansible/awx/issues/6746)
- Added the ability to configure the destination and maximum disk size of rsyslogd spool (in the event of a log aggregator outage) (https://github.com/ansible/awx/pull/6763)
- Added the ability to discover playbooks in project clones from symlinked directories (https://github.com/ansible/awx/pull/6773)
- Fixed a bug that caused certain log aggregator settings to break logging integration (https://github.com/ansible/awx/issues/6760)
- Fixed a bug that caused playbook execution in container groups to sometimes unexpectedly deadlock (https://github.com/ansible/awx/issues/6692)
- Improved stability of the new redis clustering implementation (https://github.com/ansible/awx/pull/6739 https://github.com/ansible/awx/pull/6720)
- Improved stability of the new rsyslogd-based logging implementation (https://github.com/ansible/awx/pull/6796)
## 11.0.0 (Apr 16, 2020)
- As of AWX 11.0.0, Kubernetes-based deployments use a Deployment rather than a StatefulSet.
- Reimplemented external logging support using rsyslogd to improve reliability and address a number of issues (https://github.com/ansible/awx/issues/5155)
- Changed activity stream logs to include summary fields for related objects (https://github.com/ansible/awx/issues/1761)
- Added code to more gracefully attempt to reconnect to redis if it restarts/becomes unavailable (https://github.com/ansible/awx/pull/6670)
- Fixed a bug that caused REFRESH_TOKEN_EXPIRE_SECONDS to not properly be respected for OAuth2.0 refresh tokens generated by AWX (https://github.com/ansible/awx/issues/6630)
- Fixed a bug that broke schedules containing RRULES with very old DTSTART dates (https://github.com/ansible/awx/pull/6550)
- Fixed a bug that broke installs on older versions of Ansible packaged with certain Linux distributions (https://github.com/ansible/awx/issues/5501)
- Fixed a bug that caused the activity stream to sometimes report the incorrect actor when associating user membership on SAML login (https://github.com/ansible/awx/pull/6525)
- Fixed a bug in AWX's Grafana notification support when annotation tags are omitted (https://github.com/ansible/awx/issues/6580)
- Fixed a bug that prevented some users from searching for Source Control credentials in the AWX user interface (https://github.com/ansible/awx/issues/6600)
- Fixed a bug that prevented disassociating orphaned users from credentials (https://github.com/ansible/awx/pull/6554)
- Updated Twisted to address CVE-2020-10108 and CVE-2020-10109.
## 10.0.0 (Mar 30, 2020)
- As of AWX 10.0.0, the official AWX CLI no longer supports Python 2 (it requires at least Python 3.6) (https://github.com/ansible/awx/pull/6327)
- AWX no longer relies on RabbitMQ; Redis is added as a new dependency (https://github.com/ansible/awx/issues/5443)
- Altered AWX's event tables to allow more than ~2 billion total events (https://github.com/ansible/awx/issues/6010)
- Improved the performance (time to execute, and memory consumption) of the periodic job cleanup system job (https://github.com/ansible/awx/pull/6166)
- Updated Job Templates so they now have an explicit Organization field (it is no longer inferred from the associated Project) (https://github.com/ansible/awx/issues/3903)
- Updated social-auth-core to address an upcoming GitHub API deprecation (https://github.com/ansible/awx/issues/5970)
- Updated to ansible-runner 1.4.6 to address various bugs.
- Updated Django to address CVE-2020-9402
- Updated pyyaml version to address CVE-2017-18342
- Fixed a bug which prevented the new `scm_branch` field from being used in custom notification templates (https://github.com/ansible/awx/issues/6258)
- Fixed a race condition that sometimes causes success/failure notifications to include an incomplete list of hosts (https://github.com/ansible/awx/pull/6290)
- Fixed a bug that can cause certain setting pages to lose unsaved form edits when a playbook is launched (https://github.com/ansible/awx/issues/5265)
- Fixed a bug that can prevent the "Use TLS/SSL" field from properly saving when editing email notification templates (https://github.com/ansible/awx/issues/6383)
- Fixed a race condition that sometimes broke event/stdout processing for jobs launched in container groups (https://github.com/ansible/awx/issues/6280)
## 9.3.0 (Mar 12, 2020)
- Added the ability to specify an OAuth2 token description in the AWX CLI (https://github.com/ansible/awx/issues/6122)
- Added support for K8S service account annotations to the installer (https://github.com/ansible/awx/pull/6007)
- Added support for K8S imagePullSecrets to the installer (https://github.com/ansible/awx/pull/5989)
- Launching jobs (and workflows) using the --monitor flag in the AWX CLI now returns a non-zero exit code on job failure (https://github.com/ansible/awx/issues/5920)
- Improved UI performance for various job views when many simultaneous users are logged into AWX (https://github.com/ansible/awx/issues/5883)
- Updated to the latest version of Django to address a few open CVEs (https://github.com/ansible/awx/pull/6080)
- Fixed a critical bug which can cause AWX to hang and stop launching playbooks after a periodic of time (https://github.com/ansible/awx/issues/5617)
- Fixed a bug which caused delays in project update stdout for certain large SCM clones (as of Ansible 2.9+) (https://github.com/ansible/awx/pull/6254)
- Fixed a bug which caused certain smart inventory filters to mistakenly return duplicate hosts (https://github.com/ansible/awx/pull/5972)
- Fixed an unclear server error when creating smart inventories with the AWX collection (https://github.com/ansible/awx/issues/6250)
- Fixed a bug that broke Grafana notification support (https://github.com/ansible/awx/issues/6137)
- Fixed a UI bug which prevent users with read access to an organization from editing credentials for that organization (https://github.com/ansible/awx/pull/6241)
- Fixed a bug which prevent workflow approval records from recording a `started` and `elapsed` date (https://github.com/ansible/awx/issues/6202)
- Fixed a bug which caused workflow nodes to have a confusing option for `verbosity` (https://github.com/ansible/awx/issues/6196)
- Fixed an RBAC bug which prevented projects and inventory schedules from being created by certain users in certain contexts (https://github.com/ansible/awx/issues/5717)
- Fixed a bug that caused `role_path` in a project's config to not be respected due to an error processing `/etc/ansible/ansible.cfg` (https://github.com/ansible/awx/pull/6038)
- Fixed a bug that broke inventory updates for installs with custom home directories for the awx user (https://github.com/ansible/awx/pull/6152)
- Fixed a bug that broke fact data collection when AWX encounters invalid/unexpected fact data (https://github.com/ansible/awx/issues/5935)
## 9.2.0 (Feb 12, 2020)
- Added the ability to configure the convergence behavior of workflow nodes https://github.com/ansible/awx/issues/3054
- AWX now allows for a configurable global limit for fork count (per-job run). The default maximum is 200. https://github.com/ansible/awx/pull/5604
- Added the ability to specify AZURE_PUBLIC_CLOUD (for e.g., Azure Government KeyVault support) for the Azure credential plugin https://github.com/ansible/awx/issues/5138
- Added support for several additional parameters for Satellite dynamic inventory https://github.com/ansible/awx/pull/5598
- Added a new field to jobs for tracking the date/time a job is cancelled https://github.com/ansible/awx/pull/5610
- Made a series of additional optimizations to the callback receiver to further improve stdout write speed for running playbooks https://github.com/ansible/awx/pull/5677 https://github.com/ansible/awx/pull/5739
- Updated AWX to be compatible with Helm 3.x (https://github.com/ansible/awx/pull/5776)
- Optimized AWX's job dependency/scheduling code to drastically improve processing time in scenarios where there are many pending jobs scheduled simultaneously https://github.com/ansible/awx/issues/5154
- Fixed a bug which could cause SCM authentication details (basic auth passwords) to be reported to external loggers in certain failure scenarios (e.g., when a git clone fails and ansible itself prints an error message to stdout) https://github.com/ansible/awx/pull/5812
- Fixed a k8s installer bug that caused installs to fail in certain situations https://github.com/ansible/awx/issues/5574
- Fixed a number of issues that caused analytics gathering and reporting to run more often than necessary https://github.com/ansible/awx/pull/5721
- Fixed a bug in the AWX CLI that prevented JSON-type settings from saving properly https://github.com/ansible/awx/issues/5528
- Improved support for fetching custom virtualenv dependencies when AWX is installed behind a proxy https://github.com/ansible/awx/pull/5805
- Updated the bundled version of openstacksdk to address a known issue https://github.com/ansible/awx/issues/5821
- Updated the bundled vmware_inventory plugin to the latest version to address a bug https://github.com/ansible/awx/pull/5668
- Fixed a bug that can cause inventory updates to fail to properly save their output when run within a workflow https://github.com/ansible/awx/pull/5666
- Removed a number of pre-computed fields from the Host and Group models to improve AWX performance. As part of this change, inventory group UIs throughout the interface no longer display status icons https://github.com/ansible/awx/pull/5448
## 9.1.1 (Jan 14, 2020)
- Fixed a bug that caused database migrations on Kubernetes installs to hang https://github.com/ansible/awx/pull/5579
- Upgraded Python-level app dependencies in AWX virtual environment https://github.com/ansible/awx/pull/5407
- Running jobs no longer block associated inventory updates https://github.com/ansible/awx/pull/5519
- Fixed invalid_response SAML error https://github.com/ansible/awx/pull/5577
- Optimized the callback receiver to drastically improve the write speed of stdout for parallel jobs (https://github.com/ansible/awx/pull/5618)
## 9.1.0 (Dec 17, 2019)
- Added a command to generate a new SECRET_KEY and rekey the secrets in the database
- Removed project update locking when jobs using it are running
- Fixed slow queries for /api/v2/instances and /api/v2/instance_groups when smart inventories are used
- Fixed a partial password disclosure when special characters existed in the RabbitMQ password (CVE-2019-19342)
- Fixed hang in error handling for source control checkouts
- Fixed an error on subsequent job runs that override the branch of a project on an instance that did not have a prior project checkout
- Fixed an issue where jobs launched in isolated or container groups would incorrectly timeout
- Fixed an incorrect link to instance groups documentation in the user interface
- Fixed editing of inventory on Workflow templates
- Fixed multiple issues with OAuth2 token cleanup system jobs
- Fixed a bug that broke email notifications for workflow approval/deny https://github.com/ansible/awx/issues/5401
- Updated SAML implementation to automatically login if authorization already exists
- Updated AngularJS to 1.7.9 for CVE-2019-10768
## 9.0.1 (Nov 4, 2019)
- Fixed a bug in the installer that broke certain types of k8s installs https://github.com/ansible/awx/issues/5205
## 9.0.0 (Oct 31, 2019)
- Updated AWX images to use centos:8 as the parent image.
- Updated to ansible-runner 1.4.4 to address various bugs.
- Added oc and kubectl to the AWX images to support new container-based execution introduced in 8.0.0.
- Added some optimizations to speed up the deletion of large Inventory Groups.
- Fixed a bug that broke webhook launches for Job Templates that define a survey (https://github.com/ansible/awx/issues/5062).
- Fixed a bug in the CLI which incorrectly parsed launch time arguments for `awx job_templates launch` and `awx workflow_job_templates launch` (https://github.com/ansible/awx/issues/5093).
- Fixed a bug that caused inventory updates using "sourced from a project" to stop working (https://github.com/ansible/awx/issues/4750).
- Fixed a bug that caused Slack notifications to sometimes show the wrong bot avatar (https://github.com/ansible/awx/pull/5125).
- Fixed a bug that prevented the use of digits in AWX's URL settings (https://github.com/ansible/awx/issues/5081).
## 8.0.0 (Oct 21, 2019)
- The Ansible Tower Ansible modules have been migrated to a new official Ansible AWX collection: https://galaxy.ansible.com/awx/AWX
Please note that this functionality is only supported in Ansible 2.9+
- AWX now supports the ability to launch jobs from external webhooks (GitHub and GitLab integration are supported).
- AWX now supports Container Groups, a new feature that allows you to schedule and run playbooks on single-use kubernetes pods on-demand.
- AWX now supports sending notifications when Workflow steps are approved, denied, or time out.
- AWX now records the user who approved or denied Workflow steps.
- AWX now supports fetching Ansible Collections from private galaxy servers.
- AWX now checks the user's ansible.cfg for paths where role/collections may live when running project updates.
- AWX now uses PostgreSQL 10 by default.
- AWX now warns more loudly about underlying AMQP connectivity issues (https://github.com/ansible/awx/pull/4857).
- Added a few optimizations to drastically improve dashboard performance for larger AWX installs (installs with several hundred thousand jobs or more).
- Updated to the latest version of Ansible's VMWare inventory script (which adds support for vmware_guest_facts).
- Deprecated /api/v2/inventory_scripts/ (this endpoint - and the Custom Inventory Script feature - will be removed in a future release of AWX).
- Fixed a bug which prevented Organization Admins from removing users from their own Organization (https://github.com/ansible/awx/issues/2979)
- Fixed a bug which sometimes caused cluster nodes to fail to re-join with a cryptic error, "No instance found with the current cluster host id" (https://github.com/ansible/awx/issues/4294)
- Fixed a bug that prevented the use of launch-time passphrases when using credential plugins (https://github.com/ansible/awx/pull/4807)
- Fixed a bug that caused notifications assigned at the Organization level not to take effect for Workflows in that Organization (https://github.com/ansible/awx/issues/4712)
- Fixed a bug which caused a notable amount of CPU overhead on RabbitMQ health checks (https://github.com/ansible/awx/pull/5009)
- Fixed a bug which sometimes caused the <return> key to stop functioning in <textarea> elements (https://github.com/ansible/awx/issues/4192)
- Fixed a bug which caused request contention when the same OAuth2.0 token was used in multiple simultaneous requests (https://github.com/ansible/awx/issues/4694)
- Fixed a bug related to parsing multiple choice survey options (https://github.com/ansible/awx/issues/4452).
- Fixed a bug that caused single-sign-on icons on the login page to fail to render in certain Windows browsers (https://github.com/ansible/awx/issues/3924)
- Fixed a number of bugs that caused certain OAuth2 settings to not be properly respected, such as REFRESH_TOKEN_EXPIRE_SECONDS.
- Fixed a number of bugs in the AWX CLI, including a bug which sometimes caused long lines of stdout output to be unexpectedly truncated.
- Fixed a number of bugs on the job details UI which sometimes caused auto-scrolling stdout to become stuck.
- Fixed a bug which caused LDAP authentication to fail if the TLD of the server URL contained digits (https://github.com/ansible/awx/issues/3646)
- Fixed a bug which broke HashiCorp Vault integration on older versions of HashiCorp Vault.
## 7.0.0 (Sept 4, 2019)
- AWX now detects and installs Ansible Collections defined in your project (note - this feature only works in Ansible 2.9+) (https://github.com/ansible/awx/issues/2534)
- AWX now includes an official command line client. Keep an eye out for a follow-up email on this mailing list for information on how to install it and try it out.
- Added the ability to provide a specific SCM branch on jobs (https://github.com/ansible/awx/issues/282)
- Added support for Workflow Approval Nodes, a new feature which allows you to add "pause and wait for approval" steps into your workflows (https://github.com/ansible/awx/issues/1206)
- Added the ability to specify a specific HTTP method for webhook notifications (POST vs PUT) (https://github.com/ansible/awx/pull/4124)
- Added the ability to specify a username and password for HTTP Basic Authorization for webhook notifications (https://github.com/ansible/awx/pull/4124)
- Added support for customizing the text content of notifications (https://github.com/ansible/awx/issues/79)
- Added the ability to enable and disable hosts in dynamic inventory (https://github.com/ansible/awx/pull/4420)
- Added the description (if any) to the Job Template list (https://github.com/ansible/awx/issues/4359)
- Added new metrics for instance hostnames and pending jobs to the /api/v2/metrics/ endpoint (https://github.com/ansible/awx/pull/4375)
- Changed AWX's on/off toggle buttons to a non-text based style to simplify internationalization (https://github.com/ansible/awx/pull/4425)
- Events emitted by ansible for adhoc commands are now sent to the external log aggregrator (https://github.com/ansible/awx/issues/4545)
- Fixed a bug which allowed a user to make an organization credential in another organization without permissions to that organization (https://github.com/ansible/awx/pull/4483)
- Fixed a bug that caused `extra_vars` on workflows to break when edited (https://github.com/ansible/awx/issues/4293)
- Fixed a slow SQL query that caused performance issues when large numbers of groups exist (https://github.com/ansible/awx/issues/4461)
- Fixed a few minor bugs in survey field validation (https://github.com/ansible/awx/pull/4509) (https://github.com/ansible/awx/pull/4479)
- Fixed a bug that sometimes resulted in orphaned `ansible_runner_pi` directories in `/tmp` after playbook execution (https://github.com/ansible/awx/pull/4409)
- Fixed a bug that caused the `is_system_auditor` flag in LDAP configuration to not work (https://github.com/ansible/awx/pull/4396)
- Fixed a bug which caused schedules to disappear from the UI when toggled off (https://github.com/ansible/awx/pull/4378)
- Fixed a bug that sometimes caused stdout content to contain extraneous blank lines in newer versions of Ansible (https://github.com/ansible/awx/pull/4391)
- Updated to the latest Django security release, 2.2.4 (https://github.com/ansible/awx/pull/4410) (https://www.djangoproject.com/weblog/2019/aug/01/security-releases/)
- Updated the default version of git to a version that includes support for x509 certificates (https://github.com/ansible/awx/issues/4362)
- Removed the deprecated `credential` field from `/api/v2/workflow_job_templates/N/` (as part of the `/api/v1/` removal in prior AWX versions - https://github.com/ansible/awx/pull/4490).
## 6.1.0 (Jul 18, 2019)
- Updated AWX to use Django 2.2.2.
- Updated the provided openstacksdk version to support new functionality (such as Nova scheduler_hints)
- Added the ability to specify a custom cacert for the HashiCorp Vault credential plugin
- Fixed a number of bugs related to path lookups for the HashiCorp Vault credential plugin
- Fixed a bug which prevented signed SSH certificates from working, including the HashiCorp Vault Signed SSH backend
- Fixed a bug which prevented custom logos from displaying on the login page (as a result of a new Content Security Policy in 6.0.0)
- Fixed a bug which broke websocket connectivity in Apple Safari (as a result of a new Content Security Policy in 6.0.0)
- Fixed a bug on the job output page that occasionally caused the "up" and "down" buttons to not load additional output
- Fixed a bug on the job output page that caused quoted task names to display incorrectly
## 6.0.0 (Jul 1, 2019)
- Removed support for "Any" notification templates and their API endpoints e.g., /api/v2/job_templates/N/notification_templates/any/ (https://github.com/ansible/awx/issues/4022)
- Fixed a bug which prevented credentials from properly being applied to inventory sources (https://github.com/ansible/awx/issues/4059)
- Fixed a bug which can cause the task dispatcher to hang indefinitely when external logging support (e.g., Splunk, Logstash) is enabled (https://github.com/ansible/awx/issues/4181)
- Fixed a bug which causes slow stdout display when running jobs against smart inventories. (https://github.com/ansible/awx/issues/3106)
- Fixed a bug that caused SSL verification flags to fail to be respected for LDAP authentication in certain environments. (https://github.com/ansible/awx/pull/4190)
- Added a simple Content Security Policy (https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) to restrict access to third-party resources in the browser. (https://github.com/ansible/awx/pull/4167)
- Updated ovirt4 library dependencies to work with newer versions of oVirt (https://github.com/ansible/awx/issues/4138)
## 5.0.0 (Jun 21, 2019)
- Bump Django Rest Framework from 3.7.7 to 3.9.4
- Bump setuptools / pip dependencies
- Fixed bug where Recent Notification list would not appear
- Added notifications on job start
- Default to Ansible 2.8
For older release notes, see https://github.com/ansible/awx/blob/19.3.0/CHANGELOG.md.

View File

@@ -110,7 +110,7 @@ For feature work, take a look at the current [Enhancements](https://github.com/a
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](https://github.com/ansible/awx/blob/devel/docs/debugging.md).
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/).
**NOTE**

129
Makefile
View File

@@ -1,61 +1,41 @@
PYTHON ?= python3.8
PYTHON_VERSION = $(shell $(PYTHON) -c "from distutils.sysconfig import get_python_version; print(get_python_version())")
SITELIB=$(shell $(PYTHON) -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")
OFFICIAL ?= no
PACKER ?= packer
PACKER_BUILD_OPTS ?= -var 'official=$(OFFICIAL)' -var 'aw_repo_url=$(AW_REPO_URL)'
NODE ?= node
NPM_BIN ?= npm
CHROMIUM_BIN=/tmp/chrome-linux/chrome
DEPS_SCRIPT ?= packaging/bundle/deps.py
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
MANAGEMENT_COMMAND ?= awx-manage
IMAGE_REPOSITORY_AUTH ?=
IMAGE_REPOSITORY_BASE ?= https://gcr.io
VERSION := $(shell cat VERSION)
VERSION := $(shell $(PYTHON) setup.py --version)
COLLECTION_VERSION := $(shell $(PYTHON) setup.py --version | cut -d . -f 1-3)
# NOTE: This defaults the container image version to the branch that's active
COMPOSE_TAG ?= $(GIT_BRANCH)
COMPOSE_HOST ?= $(shell hostname)
MAIN_NODE_TYPE ?= hybrid
# If set to true docker-compose will also start a keycloak instance
KEYCLOAK ?= false
VENV_BASE ?= /var/lib/awx/venv/
SCL_PREFIX ?=
CELERY_SCHEDULE_FILE ?= /var/lib/awx/beat.db
VENV_BASE ?= /var/lib/awx/venv
DEV_DOCKER_TAG_BASE ?= quay.io/awx
DEVEL_IMAGE_NAME ?= $(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG)
RECEPTOR_IMAGE ?= quay.io/ansible/receptor:devel
# Python packages to install only from source (not from binary wheels)
# Comma separated list
SRC_ONLY_PKGS ?= cffi,pycparser,psycopg2,twilio
# These should be upgraded in the AWX and Ansible venv before attempting
# to install the actual requirements
VENV_BOOTSTRAP ?= pip==19.3.1 setuptools==41.6.0 wheel==0.36.2
# Determine appropriate shasum command
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
SHASUM_BIN ?= sha256sum
endif
ifeq ($(UNAME_S),Darwin)
SHASUM_BIN ?= shasum -a 256
endif
# Get the branch information from git
GIT_DATE := $(shell git log -n 1 --format="%ai")
DATE := $(shell date -u +%Y%m%d%H%M)
VENV_BOOTSTRAP ?= pip==21.2.4 setuptools==58.2.0 wheel==0.36.2
NAME ?= awx
GIT_REMOTE_URL = $(shell git config --get remote.origin.url)
# TAR build parameters
SDIST_TAR_NAME=$(NAME)-$(VERSION)
WHEEL_NAME=$(NAME)-$(VERSION)
SDIST_COMMAND ?= sdist
WHEEL_COMMAND ?= bdist_wheel
SDIST_TAR_FILE ?= $(SDIST_TAR_NAME).tar.gz
WHEEL_FILE ?= $(WHEEL_NAME)-py2-none-any.whl
I18N_FLAG_FILE = .i18n_built
@@ -166,15 +146,6 @@ version_file:
fi; \
$(PYTHON) -c "import awx; print(awx.__version__)" > /var/lib/awx/.awx_version; \
# Do any one-time init tasks.
comma := ,
init:
if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/awx/bin/activate; \
fi; \
$(MANAGEMENT_COMMAND) provision_instance --hostname=$(COMPOSE_HOST); \
$(MANAGEMENT_COMMAND) register_queue --queuename=controlplane --instance_percent=100;
# Refresh development environment after pulling new code.
refresh: clean requirements_dev version_file develop migrate
@@ -305,7 +276,6 @@ test:
. $(VENV_BASE)/awx/bin/activate; \
fi; \
PYTHONDONTWRITEBYTECODE=1 py.test -p no:cacheprovider -n auto $(TEST_DIRS)
cmp VERSION awxkit/VERSION || "VERSION and awxkit/VERSION *must* match"
cd awxkit && $(VENV_BASE)/awx/bin/tox -re py3
awx-manage check_migrations --dry-run --check -n 'missing_migration_file'
@@ -337,12 +307,16 @@ symlink_collection:
ln -s $(shell pwd)/awx_collection $(COLLECTION_INSTALL)
build_collection:
ansible-playbook -i localhost, awx_collection/tools/template_galaxy.yml -e collection_package=$(COLLECTION_PACKAGE) -e collection_namespace=$(COLLECTION_NAMESPACE) -e collection_version=$(VERSION) -e '{"awx_template_version":false}'
ansible-playbook -i localhost, awx_collection/tools/template_galaxy.yml \
-e collection_package=$(COLLECTION_PACKAGE) \
-e collection_namespace=$(COLLECTION_NAMESPACE) \
-e collection_version=$(COLLECTION_VERSION) \
-e '{"awx_template_version":false}'
ansible-galaxy collection build awx_collection_build --force --output-path=awx_collection_build
install_collection: build_collection
rm -rf $(COLLECTION_INSTALL)
ansible-galaxy collection install awx_collection_build/$(COLLECTION_NAMESPACE)-$(COLLECTION_PACKAGE)-$(VERSION).tar.gz
ansible-galaxy collection install awx_collection_build/$(COLLECTION_NAMESPACE)-$(COLLECTION_PACKAGE)-$(COLLECTION_VERSION).tar.gz
test_collection_sanity: install_collection
cd $(COLLECTION_INSTALL) && ansible-test sanity
@@ -395,7 +369,7 @@ clean-ui:
awx/ui/node_modules:
NODE_OPTIONS=--max-old-space-size=4096 $(NPM_BIN) --prefix awx/ui --loglevel warn ci
$(UI_BUILD_FLAG_FILE):
$(UI_BUILD_FLAG_FILE): 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
@@ -407,7 +381,9 @@ $(UI_BUILD_FLAG_FILE):
cp -r awx/ui/build/static/media/* awx/public/static/media
touch $@
ui-release: awx/ui/node_modules $(UI_BUILD_FLAG_FILE)
ui-release: $(UI_BUILD_FLAG_FILE)
ui-devel: awx/ui/node_modules
@$(MAKE) -B $(UI_BUILD_FLAG_FILE)
@@ -425,7 +401,7 @@ ui-lint:
ui-test:
$(NPM_BIN) --prefix awx/ui install
$(NPM_BIN) run --prefix awx/ui test -- --coverage --maxWorkers=4 --watchAll=false
$(NPM_BIN) run --prefix awx/ui test
# Build a pip-installable package into dist/ with a timestamped version number.
@@ -436,33 +412,22 @@ dev_build:
release_build:
$(PYTHON) setup.py release_build
dist/$(SDIST_TAR_FILE): ui-release VERSION
HEADLESS ?= no
ifeq ($(HEADLESS), yes)
dist/$(SDIST_TAR_FILE):
else
dist/$(SDIST_TAR_FILE): $(UI_BUILD_FLAG_FILE)
endif
$(PYTHON) setup.py $(SDIST_COMMAND)
dist/$(WHEEL_FILE): ui-release
$(PYTHON) setup.py $(WHEEL_COMMAND)
ln -sf $(SDIST_TAR_FILE) dist/awx.tar.gz
sdist: dist/$(SDIST_TAR_FILE)
echo $(HEADLESS)
@echo "#############################################"
@echo "Artifacts:"
@echo dist/$(SDIST_TAR_FILE)
@echo "#############################################"
wheel: dist/$(WHEEL_FILE)
@echo "#############################################"
@echo "Artifacts:"
@echo dist/$(WHEEL_FILE)
@echo "#############################################"
# Build setup bundle tarball
setup-bundle-build:
mkdir -p $@
docker-auth:
@if [ "$(IMAGE_REPOSITORY_AUTH)" ]; then \
echo "$(IMAGE_REPOSITORY_AUTH)" | docker login -u oauth2accesstoken --password-stdin $(IMAGE_REPOSITORY_BASE); \
fi;
# This directory is bind-mounted inside of the development container and
# needs to be pre-created for permissions to be set correctly. Otherwise,
# Docker will create this directory as root.
@@ -470,7 +435,9 @@ awx/projects:
@mkdir -p $@
COMPOSE_UP_OPTS ?=
CLUSTER_NODE_COUNT ?= 1
COMPOSE_OPTS ?=
CONTROL_PLANE_NODE_COUNT ?= 1
EXECUTION_NODE_COUNT ?= 2
MINIKUBE_CONTAINER_GROUP ?= false
docker-compose-sources: .git/hooks/pre-commit
@@ -481,18 +448,21 @@ docker-compose-sources: .git/hooks/pre-commit
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/sources.yml \
-e awx_image=$(DEV_DOCKER_TAG_BASE)/awx_devel \
-e awx_image_tag=$(COMPOSE_TAG) \
-e cluster_node_count=$(CLUSTER_NODE_COUNT) \
-e minikube_container_group=$(MINIKUBE_CONTAINER_GROUP)
-e receptor_image=$(RECEPTOR_IMAGE) \
-e control_plane_node_count=$(CONTROL_PLANE_NODE_COUNT) \
-e execution_node_count=$(EXECUTION_NODE_COUNT) \
-e minikube_container_group=$(MINIKUBE_CONTAINER_GROUP) \
-e enable_keycloak=$(KEYCLOAK)
docker-compose: docker-auth awx/projects docker-compose-sources
docker-compose -f tools/docker-compose/_sources/docker-compose.yml up $(COMPOSE_UP_OPTS)
docker-compose: awx/projects docker-compose-sources
docker-compose -f tools/docker-compose/_sources/docker-compose.yml $(COMPOSE_OPTS) up $(COMPOSE_UP_OPTS) --remove-orphans
docker-compose-credential-plugins: docker-auth awx/projects docker-compose-sources
docker-compose-credential-plugins: awx/projects docker-compose-sources
echo -e "\033[0;31mTo generate a CyberArk Conjur API key: docker exec -it tools_conjur_1 conjurctl account create quick-start\033[0m"
docker-compose -f tools/docker-compose/_sources/docker-compose.yml -f tools/docker-credential-plugins-override.yml up --no-recreate awx_1
docker-compose -f tools/docker-compose/_sources/docker-compose.yml -f tools/docker-credential-plugins-override.yml up --no-recreate awx_1 --remove-orphans
docker-compose-test: docker-auth awx/projects docker-compose-sources
docker-compose-test: awx/projects docker-compose-sources
docker-compose -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports awx_1 /bin/bash
docker-compose-runtest: awx/projects docker-compose-sources
@@ -517,14 +487,16 @@ docker-compose-container-group-clean:
# Base development image build
docker-compose-build:
ansible-playbook tools/ansible/dockerfile.yml -e build_dev=True
ansible-playbook tools/ansible/dockerfile.yml -e build_dev=True -e receptor_image=$(RECEPTOR_IMAGE)
DOCKER_BUILDKIT=1 docker build -t $(DEVEL_IMAGE_NAME) \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--cache-from=$(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG) .
docker-clean:
$(foreach container_id,$(shell docker ps -f name=tools_awx -aq),docker stop $(container_id); docker rm -f $(container_id);)
docker images | grep "awx_devel" | awk '{print $$1 ":" $$2}' | xargs docker rmi
$(foreach container_id,$(shell docker ps -f name=tools_awx -aq && docker ps -f name=tools_receptor -aq),docker stop $(container_id); docker rm -f $(container_id);)
if [ "$(shell docker images | grep awx_devel)" ]; then \
docker images | grep awx_devel | awk '{print $$3}' | xargs docker rmi --force; \
fi
docker-clean-volumes: docker-compose-clean docker-compose-container-group-clean
docker volume rm tools_awx_db
@@ -532,10 +504,10 @@ docker-clean-volumes: docker-compose-clean docker-compose-container-group-clean
docker-refresh: docker-clean docker-compose
# Docker Development Environment with Elastic Stack Connected
docker-compose-elk: docker-auth awx/projects docker-compose-sources
docker-compose-elk: awx/projects docker-compose-sources
docker-compose -f tools/docker-compose/_sources/docker-compose.yml -f tools/elastic/docker-compose.logstash-link.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate
docker-compose-cluster-elk: docker-auth awx/projects docker-compose-sources
docker-compose-cluster-elk: awx/projects docker-compose-sources
docker-compose -f tools/docker-compose/_sources/docker-compose.yml -f tools/elastic/docker-compose.logstash-link-cluster.yml -f tools/elastic/docker-compose.elastic-override.yml up --no-recreate
prometheus:
@@ -559,13 +531,14 @@ VERSION:
@echo "awx: $(VERSION)"
Dockerfile: tools/ansible/roles/dockerfile/templates/Dockerfile.j2
ansible-playbook tools/ansible/dockerfile.yml
ansible-playbook tools/ansible/dockerfile.yml -e receptor_image=$(RECEPTOR_IMAGE)
Dockerfile.kube-dev: tools/ansible/roles/dockerfile/templates/Dockerfile.j2
ansible-playbook tools/ansible/dockerfile.yml \
-e dockerfile_name=Dockerfile.kube-dev \
-e kube_dev=True \
-e template_dest=_build_kube_dev
-e template_dest=_build_kube_dev \
-e receptor_image=$(RECEPTOR_IMAGE)
awx-kube-dev-build: Dockerfile.kube-dev
docker build -f Dockerfile.kube-dev \

View File

@@ -1,4 +1,4 @@
[![.github/workflows/ci.yml](https://github.com/ansible/awx/actions/workflows/ci.yml/badge.svg)](https://github.com/ansible/awx/actions/workflows/ci.yml) [![Code of Conduct](https://img.shields.io/badge/code%20of%20conduct-Ansible-yellow.svg)](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) [![Apache v2 License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](https://github.com/ansible/awx/blob/devel/LICENSE.md) [![AWX Mailing List](https://img.shields.io/badge/mailing%20list-AWX-orange.svg)](https://groups.google.com/g/awx-project)
[![CI](https://github.com/ansible/awx/actions/workflows/ci.yml/badge.svg?branch=devel)](https://github.com/ansible/awx/actions/workflows/ci.yml) [![Code of Conduct](https://img.shields.io/badge/code%20of%20conduct-Ansible-yellow.svg)](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) [![Apache v2 License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](https://github.com/ansible/awx/blob/devel/LICENSE.md) [![AWX Mailing List](https://img.shields.io/badge/mailing%20list-AWX-orange.svg)](https://groups.google.com/g/awx-project)
[![IRC Chat - #ansible-awx](https://img.shields.io/badge/IRC-%23ansible--awx-blueviolet.svg)](https://libera.chat)
<img src="https://raw.githubusercontent.com/ansible/awx-logos/master/awx/ui/client/assets/logo-login.svg?sanitize=true" width=200 alt="AWX" />

View File

@@ -1 +0,0 @@
19.3.0

View File

@@ -151,7 +151,7 @@ def manage():
from django.core.management import execute_from_command_line
# enforce the postgres version is equal to 12. if not, then terminate program with exit code of 1
if not MODE == 'development':
if not os.getenv('SKIP_PG_VERSION_CHECK', False) and not MODE == 'development':
if (connection.pg_version // 10000) < 12:
sys.stderr.write("Postgres version 12 is required\n")
sys.exit(1)

View File

@@ -44,6 +44,7 @@ from awx.main.views import ApiErrorView
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer
from awx.api.versioning import URLPathVersioning
from awx.api.metadata import SublistAttachDetatchMetadata, Metadata
from awx.conf import settings_registry
__all__ = [
'APIView',
@@ -208,12 +209,20 @@ class APIView(views.APIView):
return response
if response.status_code >= 400:
status_msg = "status %s received by user %s attempting to access %s from %s" % (
response.status_code,
request.user,
request.path,
request.META.get('REMOTE_ADDR', None),
)
msg_data = {
'status_code': response.status_code,
'user_name': request.user,
'url_path': request.path,
'remote_addr': request.META.get('REMOTE_ADDR', None),
'error': response.data.get('error', response.status_text),
}
try:
status_msg = getattr(settings, 'API_400_ERROR_LOG_FORMAT').format(**msg_data)
except Exception as e:
if getattr(settings, 'API_400_ERROR_LOG_FORMAT', None):
logger.error("Unable to format API_400_ERROR_LOG_FORMAT setting, defaulting log message: {}".format(e))
status_msg = settings_registry.get_setting_field('API_400_ERROR_LOG_FORMAT').get_default().format(**msg_data)
if hasattr(self, '__init_request_error__'):
response = self.handle_exception(self.__init_request_error__)
if response.status_code == 401:
@@ -221,6 +230,7 @@ class APIView(views.APIView):
logger.info(status_msg)
else:
logger.warning(status_msg)
response = super(APIView, self).finalize_response(request, response, *args, **kwargs)
time_started = getattr(self, 'time_started', None)
response['X-API-Product-Version'] = get_awx_version()
@@ -817,7 +827,7 @@ class ResourceAccessList(ParentMixin, ListAPIView):
def trigger_delayed_deep_copy(*args, **kwargs):
from awx.main.tasks import deep_copy_model_obj
from awx.main.tasks.system import deep_copy_model_obj
connection.on_commit(lambda: deep_copy_model_obj.delay(*args, **kwargs))

View File

@@ -25,7 +25,7 @@ __all__ = [
'ProjectUpdatePermission',
'InventoryInventorySourcesUpdatePermission',
'UserPermission',
'IsSuperUser',
'IsSystemAdminOrAuditor',
'InstanceGroupTowerPermission',
'WorkflowApprovalPermission',
]
@@ -236,13 +236,18 @@ class UserPermission(ModelAccessPermission):
raise PermissionDenied()
class IsSuperUser(permissions.BasePermission):
class IsSystemAdminOrAuditor(permissions.BasePermission):
"""
Allows access only to admin users.
Allows write access only to system admin users.
Allows read access only to system auditor users.
"""
def has_permission(self, request, view):
return request.user and request.user.is_superuser
if not (request.user and request.user.is_authenticated):
return False
if request.method == 'GET':
return request.user.is_superuser or request.user.is_system_auditor
return request.user.is_superuser
class InstanceGroupTowerPermission(ModelAccessPermission):

View File

@@ -57,6 +57,7 @@ from awx.main.models import (
Host,
Instance,
InstanceGroup,
InstanceLink,
Inventory,
InventorySource,
InventoryUpdate,
@@ -4767,6 +4768,28 @@ class ScheduleSerializer(LaunchConfigurationBaseSerializer, SchedulePreviewSeria
return super(ScheduleSerializer, self).validate(attrs)
class InstanceLinkSerializer(BaseSerializer):
class Meta:
model = InstanceLink
fields = ('source', 'target')
source = serializers.SlugRelatedField(slug_field="hostname", read_only=True)
target = serializers.SlugRelatedField(slug_field="hostname", read_only=True)
class InstanceNodeSerializer(BaseSerializer):
class Meta:
model = Instance
fields = ('id', 'hostname', 'node_type', 'node_state')
node_state = serializers.SerializerMethodField()
def get_node_state(self, obj):
if not obj.enabled:
return "disabled"
return "unhealthy" if obj.errors else "healthy"
class InstanceSerializer(BaseSerializer):
consumed_capacity = serializers.SerializerMethodField()
@@ -4786,6 +4809,9 @@ class InstanceSerializer(BaseSerializer):
"hostname",
"created",
"modified",
"last_seen",
"last_health_check",
"errors",
'capacity_adjustment',
"version",
"capacity",
@@ -4806,6 +4832,8 @@ class InstanceSerializer(BaseSerializer):
res = super(InstanceSerializer, self).get_related(obj)
res['jobs'] = self.reverse('api:instance_unified_jobs_list', kwargs={'pk': obj.pk})
res['instance_groups'] = self.reverse('api:instance_instance_groups_list', kwargs={'pk': obj.pk})
if self.context['request'].user.is_superuser or self.context['request'].user.is_system_auditor:
res['health_check'] = self.reverse('api:instance_health_check', kwargs={'pk': obj.pk})
return res
def get_consumed_capacity(self, obj):
@@ -4818,6 +4846,13 @@ class InstanceSerializer(BaseSerializer):
return float("{0:.2f}".format(((float(obj.capacity) - float(obj.consumed_capacity)) / (float(obj.capacity))) * 100))
class InstanceHealthCheckSerializer(BaseSerializer):
class Meta:
model = Instance
read_only_fields = ('uuid', 'hostname', 'version', 'last_health_check', 'errors', 'cpu', 'memory', 'cpu_capacity', 'mem_capacity', 'capacity')
fields = read_only_fields
class InstanceGroupSerializer(BaseSerializer):
show_capabilities = ['edit', 'delete']
@@ -4991,6 +5026,7 @@ class ActivityStreamSerializer(BaseSerializer):
('credential_type', ('id', 'name', 'description', 'kind', 'managed')),
('ad_hoc_command', ('id', 'name', 'status', 'limit')),
('workflow_approval', ('id', 'name', 'unified_job_id')),
('instance', ('id', 'hostname')),
]
return field_list

View File

@@ -0,0 +1,33 @@
{% ifmeth GET %}
# Health Check Data
Health checks are used to obtain important data about an instance.
Instance fields affected by the health check are shown in this view.
Fundamentally, health checks require running code on the machine in question.
- For instances with `node_type` of "control" or "hybrid", health checks are
performed as part of a periodic task that runs in the background.
- For instances with `node_type` of "execution", health checks are done by submitting
a work unit through the receptor mesh.
If ran through the receptor mesh, the invoked command is:
```
ansible-runner worker --worker-info
```
For execution nodes, these checks are _not_ performed on a regular basis.
Health checks against functional nodes will be ran when the node is first discovered.
Health checks against nodes with errors will be repeated at a reduced frequency.
{% endifmeth %}
{% ifmeth POST %}
# Manually Initiate a Health Check
For purposes of error remediation or debugging, a health check can be
manually initiated by making a POST request to this endpoint.
This will submit the work unit to the target node through the receptor mesh and wait for it to finish.
The model will be updated with the result.
Up-to-date values of the fields will be returned in the response data.
{% endifmeth %}

View File

@@ -0,0 +1 @@
Make a GET request to this resource to obtain a list all Receptor Nodes and their links.

View File

@@ -3,7 +3,7 @@
from django.conf.urls import url
from awx.api.views import InstanceList, InstanceDetail, InstanceUnifiedJobsList, InstanceInstanceGroupsList
from awx.api.views import InstanceList, InstanceDetail, InstanceUnifiedJobsList, InstanceInstanceGroupsList, InstanceHealthCheck
urls = [
@@ -11,6 +11,7 @@ urls = [
url(r'^(?P<pk>[0-9]+)/$', InstanceDetail.as_view(), name='instance_detail'),
url(r'^(?P<pk>[0-9]+)/jobs/$', InstanceUnifiedJobsList.as_view(), name='instance_unified_jobs_list'),
url(r'^(?P<pk>[0-9]+)/instance_groups/$', InstanceInstanceGroupsList.as_view(), name='instance_instance_groups_list'),
url(r'^(?P<pk>[0-9]+)/health_check/$', InstanceHealthCheck.as_view(), name='instance_health_check'),
]
__all__ = ['urls']

View File

@@ -28,6 +28,7 @@ from awx.api.views import (
OAuth2TokenList,
ApplicationOAuth2TokenList,
OAuth2ApplicationDetail,
MeshVisualizer,
)
from awx.api.views.metrics import MetricsView
@@ -95,6 +96,7 @@ v2_urls = [
url(r'^me/$', UserMeList.as_view(), name='user_me_list'),
url(r'^dashboard/$', DashboardView.as_view(), name='dashboard_view'),
url(r'^dashboard/graphs/jobs/$', DashboardJobsGraphView.as_view(), name='dashboard_jobs_graph_view'),
url(r'^mesh_visualizer/', MeshVisualizer.as_view(), name='mesh_visualizer_view'),
url(r'^settings/', include('awx.conf.urls')),
url(r'^instances/', include(instance_urls)),
url(r'^instance_groups/', include(instance_group_urls)),

View File

@@ -62,7 +62,7 @@ import pytz
from wsgiref.util import FileWrapper
# AWX
from awx.main.tasks import send_notifications, update_inventory_computed_fields
from awx.main.tasks.system import send_notifications, update_inventory_computed_fields
from awx.main.access import get_user_queryset, HostAccess
from awx.api.generics import (
APIView,
@@ -108,6 +108,7 @@ from awx.api.permissions import (
InstanceGroupTowerPermission,
VariableDataPermission,
WorkflowApprovalPermission,
IsSystemAdminOrAuditor,
)
from awx.api import renderers
from awx.api import serializers
@@ -158,6 +159,7 @@ from awx.api.views.inventory import ( # noqa
InventoryJobTemplateList,
InventoryCopy,
)
from awx.api.views.mesh_visualizer import MeshVisualizer # noqa
from awx.api.views.root import ( # noqa
ApiRootView,
ApiOAuthAuthorizationRootView,
@@ -363,19 +365,22 @@ class InstanceList(ListAPIView):
serializer_class = serializers.InstanceSerializer
search_fields = ('hostname',)
def get_queryset(self):
return super().get_queryset().exclude(node_type='hop')
class InstanceDetail(RetrieveUpdateAPIView):
name = _("Instance Detail")
model = models.Instance
queryset = models.Instance.objects.exclude(node_type='hop')
serializer_class = serializers.InstanceSerializer
def update(self, request, *args, **kwargs):
r = super(InstanceDetail, self).update(request, *args, **kwargs)
if status.is_success(r.status_code):
obj = self.get_object()
obj.refresh_capacity()
obj.save()
obj.set_capacity_value()
obj.save(update_fields=['capacity'])
r.data = serializers.InstanceSerializer(obj, context=self.get_serializer_context()).to_representation(obj)
return r
@@ -402,6 +407,63 @@ class InstanceInstanceGroupsList(InstanceGroupMembershipMixin, SubListCreateAtta
parent_model = models.Instance
relationship = 'rampart_groups'
def is_valid_relation(self, parent, sub, created=False):
if parent.node_type == 'control':
return {'msg': _(f"Cannot change instance group membership of control-only node: {parent.hostname}.")}
if parent.node_type == 'hop':
return {'msg': _(f"Cannot change instance group membership of hop node: {parent.hostname}.")}
return None
class InstanceHealthCheck(GenericAPIView):
name = _('Instance Health Check')
queryset = models.Instance.objects.exclude(node_type='hop')
serializer_class = serializers.InstanceHealthCheckSerializer
permission_classes = (IsSystemAdminOrAuditor,)
def get(self, request, *args, **kwargs):
obj = self.get_object()
data = self.get_serializer(data=request.data).to_representation(obj)
return Response(data, status=status.HTTP_200_OK)
def post(self, request, *args, **kwargs):
obj = self.get_object()
if obj.node_type == 'execution':
from awx.main.tasks.system import execution_node_health_check
runner_data = execution_node_health_check(obj.hostname)
obj.refresh_from_db()
data = self.get_serializer(data=request.data).to_representation(obj)
# Add in some extra unsaved fields
for extra_field in ('transmit_timing', 'run_timing'):
if extra_field in runner_data:
data[extra_field] = runner_data[extra_field]
else:
from awx.main.tasks.system import cluster_node_health_check
if settings.CLUSTER_HOST_ID == obj.hostname:
cluster_node_health_check(obj.hostname)
else:
cluster_node_health_check.apply_async([obj.hostname], queue=obj.hostname)
start_time = time.time()
prior_check_time = obj.last_health_check
while time.time() - start_time < 50.0:
obj.refresh_from_db(fields=['last_health_check'])
if obj.last_health_check != prior_check_time:
break
if time.time() - start_time < 1.0:
time.sleep(0.1)
else:
time.sleep(1.0)
else:
obj.mark_offline(errors=_('Health check initiated by user determined this instance to be unresponsive'))
obj.refresh_from_db()
data = self.get_serializer(data=request.data).to_representation(obj)
return Response(data, status=status.HTTP_200_OK)
class InstanceGroupList(ListCreateAPIView):
@@ -444,6 +506,13 @@ class InstanceGroupInstanceList(InstanceGroupMembershipMixin, SubListAttachDetac
relationship = "instances"
search_fields = ('hostname',)
def is_valid_relation(self, parent, sub, created=False):
if sub.node_type == 'control':
return {'msg': _(f"Cannot change instance group membership of control-only node: {sub.hostname}.")}
if sub.node_type == 'hop':
return {'msg': _(f"Cannot change instance group membership of hop node: {sub.hostname}.")}
return None
class ScheduleList(ListCreateAPIView):

View File

@@ -0,0 +1,25 @@
# Copyright (c) 2018 Red Hat, Inc.
# All Rights Reserved.
from django.utils.translation import ugettext_lazy as _
from awx.api.generics import APIView, Response
from awx.api.permissions import IsSystemAdminOrAuditor
from awx.api.serializers import InstanceLinkSerializer, InstanceNodeSerializer
from awx.main.models import InstanceLink, Instance
class MeshVisualizer(APIView):
name = _("Mesh Visualizer")
permission_classes = (IsSystemAdminOrAuditor,)
swagger_topic = "System Configuration"
def get(self, request, format=None):
data = {
'nodes': InstanceNodeSerializer(Instance.objects.all(), many=True).data,
'links': InstanceLinkSerializer(InstanceLink.objects.all(), many=True).data,
}
return Response(data)

View File

@@ -68,13 +68,23 @@ class InstanceGroupMembershipMixin(object):
membership.
"""
def attach_validate(self, request):
parent = self.get_parent_object()
sub_id, res = super().attach_validate(request)
if res: # handle an error
return sub_id, res
sub = get_object_or_400(self.model, pk=sub_id)
attach_errors = self.is_valid_relation(parent, sub)
if attach_errors:
return sub_id, Response(attach_errors, status=status.HTTP_400_BAD_REQUEST)
return sub_id, res
def attach(self, request, *args, **kwargs):
response = super(InstanceGroupMembershipMixin, self).attach(request, *args, **kwargs)
sub_id, res = self.attach_validate(request)
if status.is_success(response.status_code):
if self.parent_model is Instance:
ig_obj = get_object_or_400(self.model, pk=sub_id)
inst_name = ig_obj.hostname
inst_name = self.get_parent_object().hostname
else:
inst_name = get_object_or_400(self.model, pk=sub_id).hostname
with transaction.atomic():
@@ -91,11 +101,12 @@ class InstanceGroupMembershipMixin(object):
return response
def unattach_validate(self, request):
parent = self.get_parent_object()
(sub_id, res) = super(InstanceGroupMembershipMixin, self).unattach_validate(request)
if res:
return (sub_id, res)
sub = get_object_or_400(self.model, pk=sub_id)
attach_errors = self.is_valid_relation(None, sub)
attach_errors = self.is_valid_relation(parent, sub)
if attach_errors:
return (sub_id, Response(attach_errors, status=status.HTTP_400_BAD_REQUEST))
return (sub_id, res)

View File

@@ -123,6 +123,7 @@ class ApiVersionRootView(APIView):
data['workflow_approvals'] = reverse('api:workflow_approval_list', request=request)
data['workflow_job_template_nodes'] = reverse('api:workflow_job_template_node_list', request=request)
data['workflow_job_nodes'] = reverse('api:workflow_job_node_list', request=request)
data['mesh_visualizer'] = reverse('api:mesh_visualizer_view', request=request)
return Response(data)
@@ -149,16 +150,24 @@ class ApiV2PingView(APIView):
response = {'ha': is_ha_environment(), 'version': get_awx_version(), 'active_node': settings.CLUSTER_HOST_ID, 'install_uuid': settings.INSTALL_UUID}
response['instances'] = []
for instance in Instance.objects.all():
for instance in Instance.objects.exclude(node_type='hop'):
response['instances'].append(
dict(node=instance.hostname, uuid=instance.uuid, heartbeat=instance.modified, capacity=instance.capacity, version=instance.version)
dict(
node=instance.hostname,
node_type=instance.node_type,
uuid=instance.uuid,
heartbeat=instance.last_seen,
capacity=instance.capacity,
version=instance.version,
)
)
sorted(response['instances'], key=operator.itemgetter('node'))
response['instances'] = sorted(response['instances'], key=operator.itemgetter('node'))
response['instance_groups'] = []
for instance_group in InstanceGroup.objects.prefetch_related('instances'):
response['instance_groups'].append(
dict(name=instance_group.name, capacity=instance_group.capacity, instances=[x.hostname for x in instance_group.instances.all()])
)
response['instance_groups'] = sorted(response['instance_groups'], key=lambda x: x['name'].lower())
return Response(response)

View File

@@ -23,10 +23,10 @@ from rest_framework import status
# AWX
from awx.api.generics import APIView, GenericAPIView, ListAPIView, RetrieveUpdateDestroyAPIView
from awx.api.permissions import IsSuperUser
from awx.api.permissions import IsSystemAdminOrAuditor
from awx.api.versioning import reverse
from awx.main.utils import camelcase_to_underscore
from awx.main.tasks import handle_setting_changes
from awx.main.tasks.system import handle_setting_changes
from awx.conf.models import Setting
from awx.conf.serializers import SettingCategorySerializer, SettingSingletonSerializer
from awx.conf import settings_registry
@@ -150,7 +150,7 @@ class SettingLoggingTest(GenericAPIView):
name = _('Logging Connectivity Test')
model = Setting
serializer_class = SettingSingletonSerializer
permission_classes = (IsSuperUser,)
permission_classes = (IsSystemAdminOrAuditor,)
filter_backends = []
def post(self, request, *args, **kwargs):

View File

@@ -4856,7 +4856,7 @@ msgid "Exception connecting to PagerDuty: {}"
msgstr ""
#: awx/main/notifications/pagerduty_backend.py:87
#: awx/main/notifications/slack_backend.py:48
#: awx/main/notifications/slack_backend.py:49
#: awx/main/notifications/twilio_backend.py:47
msgid "Exception sending messages: {}"
msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -337,7 +337,11 @@ def _events_table(since, full_path, until, tbl, where_column, project_job_create
{tbl}.parent_uuid,
{tbl}.event,
task_action,
(CASE WHEN event = 'playbook_on_stats' THEN event_data END) as playbook_on_stats,
-- '-' operator listed here:
-- https://www.postgresql.org/docs/12/functions-json.html
-- note that operator is only supported by jsonb objects
-- https://www.postgresql.org/docs/current/datatype-json.html
(CASE WHEN event = 'playbook_on_stats' THEN {event_data} - 'artifact_data' END) as playbook_on_stats,
{tbl}.failed,
{tbl}.changed,
{tbl}.playbook,
@@ -352,14 +356,14 @@ def _events_table(since, full_path, until, tbl, where_column, project_job_create
x.duration AS duration,
x.res->'warnings' AS warnings,
x.res->'deprecations' AS deprecations
FROM {tbl}, json_to_record({event_data}) AS x("res" json, "duration" text, "task_action" text, "start" text, "end" text)
FROM {tbl}, jsonb_to_record({event_data}) AS x("res" json, "duration" text, "task_action" text, "start" text, "end" text)
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::json"), path=full_path)
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', '')::json"), path=full_path)
return _copy_table(table='events', query=query(f"replace({tbl}.event_data::text, '\\u0000', '')::jsonb"), path=full_path)
@register('events_table', '1.3', format='csv', description=_('Automation task records'), expensive=four_hour_slicing)

View File

@@ -408,6 +408,21 @@ register(
unit=_('seconds'),
)
register(
'DEFAULT_JOB_IDLE_TIMEOUT',
field_class=fields.IntegerField,
min_value=0,
default=0,
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.'
),
category=_('Jobs'),
category_slug='jobs',
unit=_('seconds'),
)
register(
'DEFAULT_INVENTORY_UPDATE_TIMEOUT',
field_class=fields.IntegerField,
@@ -659,6 +674,24 @@ register(
category=_('Logging'),
category_slug='logging',
)
register(
'API_400_ERROR_LOG_FORMAT',
field_class=fields.CharField,
default='status {status_code} received by user {user_name} attempting to access {url_path} from {remote_addr}',
label=_('Log Format For API 4XX Errors'),
help_text=_(
'The format of logged messages when an API 4XX error occurs, '
'the following variables will be substituted: \n'
'status_code - The HTTP status code of the error\n'
'user_name - The user name attempting to use the API\n'
'url_path - The URL path to the API endpoint called\n'
'remote_addr - The remote address seen for the user\n'
'error - The error set by the api endpoint\n'
'Variables need to be in the format {<variable name>}.'
),
category=_('Logging'),
category_slug='logging',
)
register(
@@ -672,7 +705,7 @@ register(
register(
'AUTOMATION_ANALYTICS_LAST_ENTRIES',
field_class=fields.CharField,
label=_('Last gathered entries for expensive collectors for Insights for Ansible Automation Platform.'),
label=_('Last gathered entries from the data collection service of Insights for Ansible Automation Platform'),
default='',
allow_blank=True,
category=_('System'),

View File

@@ -77,3 +77,11 @@ LOGGER_BLOCKLIST = (
# loggers that may be called getting logging settings
'awx.conf',
)
# Reported version for node seen in receptor mesh but for which capacity check
# failed or is in progress
RECEPTOR_PENDING = 'ansible-runner-???'
# Naming pattern for AWX jobs in /tmp folder, like /tmp/awx_42_xiwm
# also update awxkit.api.pages.unified_jobs if changed
JOB_FOLDER_PREFIX = 'awx_%s_'

View File

@@ -248,7 +248,7 @@ class WorkerPool(object):
except Exception:
logger.exception('could not fork')
else:
logger.warn('scaling up worker pid:{}'.format(worker.pid))
logger.debug('scaling up worker pid:{}'.format(worker.pid))
return idx, worker
def debug(self, *args, **kwargs):
@@ -387,7 +387,7 @@ class AutoscalePool(WorkerPool):
# more processes in the pool than we need (> min)
# send this process a message so it will exit gracefully
# at the next opportunity
logger.warn('scaling down worker pid:{}'.format(w.pid))
logger.debug('scaling down worker pid:{}'.format(w.pid))
w.quit()
self.workers.remove(w)
if w.alive:

View File

@@ -60,7 +60,7 @@ class AWXConsumerBase(object):
return f'listening on {self.queues}'
def control(self, body):
logger.warn(body)
logger.warn(f'Received control signal:\n{body}')
control = body.get('control')
if control in ('status', 'running'):
reply_queue = body['reply_to']
@@ -137,7 +137,7 @@ class AWXConsumerPG(AWXConsumerBase):
def run(self, *args, **kwargs):
super(AWXConsumerPG, self).run(*args, **kwargs)
logger.warn(f"Running worker {self.name} listening to queues {self.queues}")
logger.info(f"Running worker {self.name} listening to queues {self.queues}")
init = False
while True:
@@ -188,7 +188,7 @@ class BaseWorker(object):
if 'uuid' in body:
uuid = body['uuid']
finished.put(uuid)
logger.warn('worker exiting gracefully pid:{}'.format(os.getpid()))
logger.debug('worker exiting gracefully pid:{}'.format(os.getpid()))
def perform_work(self, body):
raise NotImplementedError()

View File

@@ -17,7 +17,7 @@ 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 import handle_success_and_failure_notifications
from awx.main.tasks.system import handle_success_and_failure_notifications
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

View File

@@ -9,7 +9,7 @@ from kubernetes.config import kube_config
from django.conf import settings
from django_guid.middleware import GuidMiddleware
from awx.main.tasks import dispatch_startup, inform_cluster_of_shutdown
from awx.main.tasks.system import dispatch_startup, inform_cluster_of_shutdown
from .base import BaseWorker
@@ -30,8 +30,8 @@ class TaskWorker(BaseWorker):
"""
Transform a dotted notation task into an imported, callable function, e.g.,
awx.main.tasks.delete_inventory
awx.main.tasks.RunProjectUpdate
awx.main.tasks.system.delete_inventory
awx.main.tasks.jobs.RunProjectUpdate
"""
if not task.startswith('awx.'):
raise ValueError('{} is not a valid awx task'.format(task))
@@ -73,15 +73,15 @@ class TaskWorker(BaseWorker):
'callbacks': [{
'args': [],
'kwargs': {}
'task': u'awx.main.tasks.handle_work_success'
'task': u'awx.main.tasks.system.handle_work_success'
}],
'errbacks': [{
'args': [],
'kwargs': {},
'task': 'awx.main.tasks.handle_work_error'
'task': 'awx.main.tasks.system.handle_work_error'
}],
'kwargs': {},
'task': u'awx.main.tasks.RunProjectUpdate'
'task': u'awx.main.tasks.jobs.RunProjectUpdate'
}
"""
settings.__clean_on_fork__()

View File

@@ -36,3 +36,7 @@ class PostRunError(Exception):
self.status = status
self.tb = tb
super(PostRunError, self).__init__(msg)
class ReceptorNodeNotFound(RuntimeError):
pass

View File

@@ -10,6 +10,6 @@ def is_ha_environment():
otherwise.
"""
# If there are two or more instances, then we are in an HA environment.
if Instance.objects.count() > 1:
if Instance.objects.filter(node_type__in=('control', 'hybrid')).count() > 1:
return True
return False

View File

@@ -23,44 +23,50 @@ class Command(BaseCommand):
with impersonate(superuser):
with disable_computed_fields():
if not Organization.objects.exists():
o = Organization.objects.create(name='Default')
o, _ = Organization.objects.get_or_create(name='Default')
p = Project(
p, _ = Project.objects.get_or_create(
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,
organization=o,
)
p.organization = o
p.save(skip_update=True)
ssh_type = CredentialType.objects.filter(namespace='ssh').first()
c = Credential.objects.create(
c, _ = Credential.objects.get_or_create(
credential_type=ssh_type, name='Demo Credential', inputs={'username': superuser.username}, created_by=superuser
)
c.admin_role.members.add(superuser)
public_galaxy_credential = Credential(
public_galaxy_credential, _ = Credential.objects.get_or_create(
name='Ansible Galaxy',
managed=True,
credential_type=CredentialType.objects.get(kind='galaxy'),
inputs={'url': 'https://galaxy.ansible.com/'},
)
public_galaxy_credential.save()
o.galaxy_credentials.add(public_galaxy_credential)
i = Inventory.objects.create(name='Demo Inventory', organization=o, created_by=superuser)
i, _ = Inventory.objects.get_or_create(name='Demo Inventory', organization=o, created_by=superuser)
Host.objects.create(
Host.objects.get_or_create(
name='localhost',
inventory=i,
variables="ansible_connection: local\nansible_python_interpreter: '{{ ansible_playbook_python }}'",
created_by=superuser,
)
jt = JobTemplate.objects.create(name='Demo Job Template', playbook='hello_world.yml', project=p, inventory=i)
jt = JobTemplate.objects.filter(name='Demo Job Template').first()
if jt:
jt.project = p
jt.inventory = i
jt.playbook = 'hello_world.yml'
jt.save()
else:
jt, _ = JobTemplate.objects.get_or_create(name='Demo Job Template', playbook='hello_world.yml', project=p, inventory=i)
jt.credentials.add(c)
print('Default organization added.')

View File

@@ -10,6 +10,7 @@ import subprocess
import sys
import time
import traceback
from collections import OrderedDict
# Django
from django.conf import settings
@@ -75,7 +76,24 @@ class AnsibleInventoryLoader(object):
bargs.extend(['-v', '{0}:{0}:Z'.format(self.source)])
for key, value in STANDARD_INVENTORY_UPDATE_ENV.items():
bargs.extend(['-e', '{0}={1}'.format(key, value)])
bargs.extend([get_default_execution_environment().image])
ee = get_default_execution_environment()
if settings.IS_K8S:
logger.warn('This command is not able to run on kubernetes-based deployment. This action should be done using the API.')
sys.exit(1)
if ee.credential:
process = subprocess.run(['podman', 'image', 'exists', ee.image], capture_output=True)
if process.returncode != 0:
logger.warn(
f'The default execution environment (id={ee.id}, name={ee.name}, image={ee.image}) is not available on this node. '
'The image needs to be available locally before using this command, due to registry authentication. '
'To pull this image, either run a job on this node or manually pull the image.'
)
sys.exit(1)
bargs.extend([ee.image])
bargs.extend(['ansible-inventory', '-i', self.source])
bargs.extend(['--playbook-dir', functioning_dir(self.source)])
if self.verbosity:
@@ -110,9 +128,7 @@ class AnsibleInventoryLoader(object):
def load(self):
base_args = self.get_base_args()
logger.info('Reading Ansible inventory source: %s', self.source)
return self.command_to_json(base_args)
@@ -137,7 +153,7 @@ class Command(BaseCommand):
type=str,
default=None,
metavar='v',
help='host variable used to ' 'set/clear enabled flag when host is online/offline, may ' 'be specified as "foo.bar" to traverse nested dicts.',
help='host variable used to set/clear enabled flag when host is online/offline, may be specified as "foo.bar" to traverse nested dicts.',
)
parser.add_argument(
'--enabled-value',
@@ -145,7 +161,7 @@ class Command(BaseCommand):
type=str,
default=None,
metavar='v',
help='value of host variable ' 'specified by --enabled-var that indicates host is ' 'enabled/online.',
help='value of host variable specified by --enabled-var that indicates host is enabled/online.',
)
parser.add_argument(
'--group-filter',
@@ -153,7 +169,7 @@ class Command(BaseCommand):
type=str,
default=None,
metavar='regex',
help='regular expression ' 'to filter group name(s); only matches are imported.',
help='regular expression to filter group name(s); only matches are imported.',
)
parser.add_argument(
'--host-filter',
@@ -161,14 +177,14 @@ class Command(BaseCommand):
type=str,
default=None,
metavar='regex',
help='regular expression ' 'to filter host name(s); only matches are imported.',
help='regular expression to filter host name(s); only matches are imported.',
)
parser.add_argument(
'--exclude-empty-groups',
dest='exclude_empty_groups',
action='store_true',
default=False,
help='when set, ' 'exclude all groups that have no child groups, hosts, or ' 'variables.',
help='when set, exclude all groups that have no child groups, hosts, or variables.',
)
parser.add_argument(
'--instance-id-var',
@@ -176,7 +192,7 @@ class Command(BaseCommand):
type=str,
default=None,
metavar='v',
help='host variable that ' 'specifies the unique, immutable instance ID, may be ' 'specified as "foo.bar" to traverse nested dicts.',
help='host variable that specifies the unique, immutable instance ID, may be specified as "foo.bar" to traverse nested dicts.',
)
def set_logging_level(self, verbosity):
@@ -269,12 +285,13 @@ class Command(BaseCommand):
self.db_instance_id_map = {}
if self.instance_id_var:
host_qs = self.inventory_source.hosts.all()
host_qs = host_qs.filter(instance_id='', variables__contains=self.instance_id_var.split('.')[0])
for host in host_qs:
instance_id = self._get_instance_id(host.variables_dict)
if not instance_id:
continue
self.db_instance_id_map[instance_id] = host.pk
for instance_id_part in reversed(self.instance_id_var.split(',')):
host_qs = host_qs.filter(instance_id='', variables__contains=instance_id_part.split('.')[0])
for host in host_qs:
instance_id = self._get_instance_id(host.variables_dict)
if not instance_id:
continue
self.db_instance_id_map[instance_id] = host.pk
def _build_mem_instance_id_map(self):
"""
@@ -300,7 +317,7 @@ class Command(BaseCommand):
self._cached_host_pk_set = frozenset(self.inventory_source.hosts.values_list('pk', flat=True))
return self._cached_host_pk_set
def _delete_hosts(self):
def _delete_hosts(self, pk_mem_host_map):
"""
For each host in the database that is NOT in the local list, delete
it. When importing from a cloud inventory source attached to a
@@ -309,25 +326,10 @@ class Command(BaseCommand):
"""
if settings.SQL_DEBUG:
queries_before = len(connection.queries)
hosts_qs = self.inventory_source.hosts
# Build list of all host pks, remove all that should not be deleted.
del_host_pks = set(self._existing_host_pks()) # makes mutable copy
if self.instance_id_var:
all_instance_ids = list(self.mem_instance_id_map.keys())
instance_ids = []
for offset in range(0, len(all_instance_ids), self._batch_size):
instance_ids = all_instance_ids[offset : (offset + self._batch_size)]
for host_pk in hosts_qs.filter(instance_id__in=instance_ids).values_list('pk', flat=True):
del_host_pks.discard(host_pk)
for host_pk in set([v for k, v in self.db_instance_id_map.items() if k in instance_ids]):
del_host_pks.discard(host_pk)
all_host_names = list(set(self.mem_instance_id_map.values()) - set(self.all_group.all_hosts.keys()))
else:
all_host_names = list(self.all_group.all_hosts.keys())
for offset in range(0, len(all_host_names), self._batch_size):
host_names = all_host_names[offset : (offset + self._batch_size)]
for host_pk in hosts_qs.filter(name__in=host_names).values_list('pk', flat=True):
del_host_pks.discard(host_pk)
del_host_pks = hosts_qs.exclude(pk__in=pk_mem_host_map.keys()).values_list('pk', flat=True)
# Now delete all remaining hosts in batches.
all_del_pks = sorted(list(del_host_pks))
for offset in range(0, len(all_del_pks), self._batch_size):
@@ -568,7 +570,63 @@ class Command(BaseCommand):
logger.debug('Host "%s" is now disabled', mem_host.name)
self._batch_add_m2m(self.inventory_source.hosts, db_host)
def _create_update_hosts(self):
def _build_pk_mem_host_map(self):
"""
Creates and returns a data structure that maps DB hosts to in-memory host that
they correspond to - meaning that those hosts will be updated to in-memory host values
"""
mem_host_pk_map = OrderedDict() # keys are mem_host name, values are matching DB host pk
host_pks_updated = set() # same as items of mem_host_pk_map but used for efficiency
mem_host_pk_map_by_id = {} # incomplete mapping by new instance_id to be sorted and pushed to mem_host_pk_map
mem_host_instance_id_map = {}
for k, v in self.all_group.all_hosts.items():
instance_id = self._get_instance_id(v.variables)
if instance_id in self.db_instance_id_map:
mem_host_pk_map_by_id[self.db_instance_id_map[instance_id]] = v
elif instance_id:
mem_host_instance_id_map[instance_id] = v
# Update all existing hosts where we know the PK based on instance_id.
all_host_pks = sorted(mem_host_pk_map_by_id.keys())
for offset in range(0, len(all_host_pks), self._batch_size):
host_pks = all_host_pks[offset : (offset + self._batch_size)]
for db_host in self.inventory.hosts.only('pk').filter(pk__in=host_pks):
if db_host.pk in host_pks_updated:
continue
mem_host = mem_host_pk_map_by_id[db_host.pk]
mem_host_pk_map[mem_host.name] = db_host.pk
host_pks_updated.add(db_host.pk)
# Update all existing hosts where we know the DB (the prior) instance_id.
all_instance_ids = sorted(mem_host_instance_id_map.keys())
for offset in range(0, len(all_instance_ids), self._batch_size):
instance_ids = all_instance_ids[offset : (offset + self._batch_size)]
for db_host in self.inventory.hosts.only('pk', 'instance_id').filter(instance_id__in=instance_ids):
if db_host.pk in host_pks_updated:
continue
mem_host = mem_host_instance_id_map[db_host.instance_id]
mem_host_pk_map[mem_host.name] = db_host.pk
host_pks_updated.add(db_host.pk)
# Update all existing hosts by name.
all_host_names = sorted(self.all_group.all_hosts.keys())
for offset in range(0, len(all_host_names), self._batch_size):
host_names = all_host_names[offset : (offset + self._batch_size)]
for db_host in self.inventory.hosts.only('pk', 'name').filter(name__in=host_names):
if db_host.pk in host_pks_updated:
continue
mem_host = self.all_group.all_hosts[db_host.name]
mem_host_pk_map[mem_host.name] = db_host.pk
host_pks_updated.add(db_host.pk)
# Rotate the dictionary so that lookups are done by the host pk
pk_mem_host_map = OrderedDict()
for name, host_pk in mem_host_pk_map.items():
pk_mem_host_map[host_pk] = name
return pk_mem_host_map # keys are DB host pk, keys are matching mem host name
def _create_update_hosts(self, pk_mem_host_map):
"""
For each host in the local list, create it if it doesn't exist in the
database. Otherwise, update/replace database variables from the
@@ -577,57 +635,22 @@ class Command(BaseCommand):
"""
if settings.SQL_DEBUG:
queries_before = len(connection.queries)
host_pks_updated = set()
mem_host_pk_map = {}
mem_host_instance_id_map = {}
mem_host_name_map = {}
mem_host_names_to_update = set(self.all_group.all_hosts.keys())
for k, v in self.all_group.all_hosts.items():
mem_host_name_map[k] = v
instance_id = self._get_instance_id(v.variables)
if instance_id in self.db_instance_id_map:
mem_host_pk_map[self.db_instance_id_map[instance_id]] = v
elif instance_id:
mem_host_instance_id_map[instance_id] = v
# Update all existing hosts where we know the PK based on instance_id.
all_host_pks = sorted(mem_host_pk_map.keys())
updated_mem_host_names = set()
all_host_pks = sorted(pk_mem_host_map.keys())
for offset in range(0, len(all_host_pks), self._batch_size):
host_pks = all_host_pks[offset : (offset + self._batch_size)]
for db_host in self.inventory.hosts.filter(pk__in=host_pks):
if db_host.pk in host_pks_updated:
continue
mem_host = mem_host_pk_map[db_host.pk]
mem_host_name = pk_mem_host_map[db_host.pk]
mem_host = self.all_group.all_hosts[mem_host_name]
self._update_db_host_from_mem_host(db_host, mem_host)
host_pks_updated.add(db_host.pk)
mem_host_names_to_update.discard(mem_host.name)
updated_mem_host_names.add(mem_host.name)
# Update all existing hosts where we know the instance_id.
all_instance_ids = sorted(mem_host_instance_id_map.keys())
for offset in range(0, len(all_instance_ids), self._batch_size):
instance_ids = all_instance_ids[offset : (offset + self._batch_size)]
for db_host in self.inventory.hosts.filter(instance_id__in=instance_ids):
if db_host.pk in host_pks_updated:
continue
mem_host = mem_host_instance_id_map[db_host.instance_id]
self._update_db_host_from_mem_host(db_host, mem_host)
host_pks_updated.add(db_host.pk)
mem_host_names_to_update.discard(mem_host.name)
# Update all existing hosts by name.
all_host_names = sorted(mem_host_name_map.keys())
for offset in range(0, len(all_host_names), self._batch_size):
host_names = all_host_names[offset : (offset + self._batch_size)]
for db_host in self.inventory.hosts.filter(name__in=host_names):
if db_host.pk in host_pks_updated:
continue
mem_host = mem_host_name_map[db_host.name]
self._update_db_host_from_mem_host(db_host, mem_host)
host_pks_updated.add(db_host.pk)
mem_host_names_to_update.discard(mem_host.name)
mem_host_names_to_create = set(self.all_group.all_hosts.keys()) - updated_mem_host_names
# Create any new hosts.
for mem_host_name in sorted(mem_host_names_to_update):
for mem_host_name in sorted(mem_host_names_to_create):
mem_host = self.all_group.all_hosts[mem_host_name]
import_vars = mem_host.variables
host_desc = import_vars.pop('_awx_description', 'imported')
@@ -726,13 +749,14 @@ class Command(BaseCommand):
self._batch_size = 500
self._build_db_instance_id_map()
self._build_mem_instance_id_map()
pk_mem_host_map = self._build_pk_mem_host_map()
if self.overwrite:
self._delete_hosts()
self._delete_hosts(pk_mem_host_map)
self._delete_groups()
self._delete_group_children_and_hosts()
self._update_inventory()
self._create_update_groups()
self._create_update_hosts()
self._create_update_hosts(pk_mem_host_map)
self._create_update_group_children()
self._create_update_group_hosts()
@@ -1008,4 +1032,4 @@ class Command(BaseCommand):
if settings.SQL_DEBUG:
queries_this_import = connection.queries[queries_before:]
sqltime = sum(float(x['time']) for x in queries_this_import)
logger.warning('Inventory import required %d queries ' 'taking %0.3fs', len(queries_this_import), sqltime)
logger.warning('Inventory import required %d queries taking %0.3fs', len(queries_this_import), sqltime)

View File

@@ -13,7 +13,7 @@ class Ungrouped(object):
@property
def instances(self):
return Instance.objects.filter(rampart_groups__isnull=True)
return Instance.objects.filter(rampart_groups__isnull=True).exclude(node_type='hop')
@property
def capacity(self):
@@ -47,7 +47,7 @@ class Command(BaseCommand):
color = '\033[90m[DISABLED] '
if no_color:
color = ''
fmt = '\t' + color + '{0.hostname} capacity={0.capacity} version={1}'
fmt = '\t' + color + '{0.hostname} capacity={0.capacity} node_type={0.node_type} version={1}'
if x.capacity:
fmt += ' heartbeat="{0.modified:%Y-%m-%d %H:%M:%S}"'
print((fmt + '\033[0m').format(x, x.version or '?'))

View File

@@ -1,6 +1,6 @@
from django.core.management.base import BaseCommand
from awx.main.tasks import profile_sql
from awx.main.tasks.system import profile_sql
class Command(BaseCommand):

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
@@ -14,18 +13,19 @@ class Command(BaseCommand):
Register this instance with the database for HA tracking.
"""
help = 'Add instance to the database. ' 'Specify `--hostname` to use this command.'
help = "Add instance to the database. Specify `--hostname` to use this command."
def add_arguments(self, parser):
parser.add_argument('--hostname', dest='hostname', type=str, help='Hostname used during provisioning')
parser.add_argument('--node_type', type=str, default="hybrid", choices=["control", "execution", "hybrid"], help='Instance Node type')
parser.add_argument('--hostname', dest='hostname', type=str, help="Hostname used during provisioning")
parser.add_argument('--node_type', type=str, default='hybrid', choices=['control', 'execution', 'hop', 'hybrid'], help="Instance Node type")
parser.add_argument('--uuid', type=str, help="Instance UUID")
def _register_hostname(self, hostname, node_type):
def _register_hostname(self, hostname, node_type, uuid):
if not hostname:
return
(changed, instance) = Instance.objects.register(uuid=self.uuid, hostname=hostname, node_type=node_type)
(changed, instance) = Instance.objects.register(hostname=hostname, node_type=node_type, uuid=uuid)
if changed:
print('Successfully registered instance {}'.format(hostname))
print("Successfully registered instance {}".format(hostname))
else:
print("Instance already registered {}".format(instance.hostname))
self.changed = changed
@@ -34,8 +34,7 @@ class Command(BaseCommand):
def handle(self, **options):
if not options.get('hostname'):
raise CommandError("Specify `--hostname` to use this command.")
self.uuid = settings.SYSTEM_UUID
self.changed = False
self._register_hostname(options.get('hostname'), options.get('node_type'))
self._register_hostname(options.get('hostname'), options.get('node_type'), options.get('uuid'))
if self.changed:
print('(changed: True)')
print("(changed: True)")

View File

@@ -0,0 +1,85 @@
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from awx.main.models import Instance, InstanceLink
class Command(BaseCommand):
"""
Internal tower command.
Register the peers of a receptor node.
"""
help = "Register or remove links between Receptor nodes."
def add_arguments(self, parser):
parser.add_argument('source', type=str, help="Receptor node opening the connections.")
parser.add_argument('--peers', type=str, nargs='+', required=False, help="Nodes that the source node connects out to.")
parser.add_argument('--disconnect', type=str, nargs='+', required=False, help="Nodes that should no longer be connected to by the source node.")
parser.add_argument(
'--exact',
type=str,
nargs='*',
required=False,
help="The exact set of nodes the source node should connect out to. Any existing links registered in the database that do not match will be removed. May be empty.",
)
def handle(self, **options):
nodes = Instance.objects.in_bulk(field_name='hostname')
if options['source'] not in nodes:
raise CommandError(f"Host {options['source']} is not a registered instance.")
if not (options['peers'] or options['disconnect'] or options['exact'] is not None):
raise CommandError("One of the options --peers, --disconnect, or --exact is required.")
if options['exact'] is not None and options['peers']:
raise CommandError("The option --peers may not be used with --exact.")
if options['exact'] is not None and options['disconnect']:
raise CommandError("The option --disconnect may not be used with --exact.")
# No 1-cycles
for collection in ('peers', 'disconnect', 'exact'):
if options[collection] is not None and options['source'] in options[collection]:
raise CommandError(f"Source node {options['source']} may not also be in --{collection}.")
# No 2-cycles
if options['peers'] or options['exact'] is not None:
peers = set(options['peers'] or options['exact'])
incoming = set(InstanceLink.objects.filter(target=nodes[options['source']]).values_list('source__hostname', flat=True))
if peers & incoming:
raise CommandError(f"Source node {options['source']} cannot link to nodes already peering to it: {peers & incoming}.")
if options['peers']:
missing_peers = set(options['peers']) - set(nodes)
if missing_peers:
missing = ' '.join(missing_peers)
raise CommandError(f"Peers not currently registered as instances: {missing}")
results = 0
for target in options['peers']:
_, created = InstanceLink.objects.get_or_create(source=nodes[options['source']], target=nodes[target])
if created:
results += 1
print(f"{results} new peer links added to the database.")
if options['disconnect']:
results = 0
for target in options['disconnect']:
if target not in nodes: # Be permissive, the node might have already been de-registered.
continue
n, _ = InstanceLink.objects.filter(source=nodes[options['source']], target=nodes[target]).delete()
results += n
print(f"{results} peer links removed from the database.")
if options['exact'] is not None:
additions = 0
with transaction.atomic():
peers = set(options['exact'])
links = set(InstanceLink.objects.filter(source=nodes[options['source']]).values_list('target__hostname', flat=True))
removals, _ = InstanceLink.objects.filter(source=nodes[options['source']], target__hostname__in=links - peers).delete()
for target in peers - links:
_, created = InstanceLink.objects.get_or_create(source=nodes[options['source']], target=nodes[target])
if created:
additions += 1
print(f"{additions} peer links added and {removals} deleted from the database.")

View File

@@ -17,13 +17,14 @@ class InstanceNotFound(Exception):
class RegisterQueue:
def __init__(self, queuename, instance_percent, inst_min, hostname_list, is_container_group=None):
def __init__(self, queuename, instance_percent, inst_min, hostname_list, is_container_group=None, pod_spec_override=None):
self.instance_not_found_err = None
self.queuename = queuename
self.instance_percent = instance_percent
self.instance_min = inst_min
self.hostname_list = hostname_list
self.is_container_group = is_container_group
self.pod_spec_override = pod_spec_override
def get_create_update_instance_group(self):
created = False
@@ -36,10 +37,14 @@ class RegisterQueue:
ig.policy_instance_minimum = self.instance_min
changed = True
if self.is_container_group:
if self.is_container_group and (ig.is_container_group != self.is_container_group):
ig.is_container_group = self.is_container_group
changed = True
if self.pod_spec_override and (ig.pod_spec_override != self.pod_spec_override):
ig.pod_spec_override = self.pod_spec_override
changed = True
if changed:
ig.save()

View File

@@ -4,16 +4,18 @@
import sys
import logging
import os
from django.db import models
from django.conf import settings
from awx.main.utils.filters import SmartFilter
from awx.main.utils.pglock import advisory_lock
from awx.main.utils.common import get_capacity_type
from awx.main.constants import RECEPTOR_PENDING
___all__ = ['HostManager', 'InstanceManager', 'InstanceGroupManager', 'DeferJobCreatedManager']
___all__ = ['HostManager', 'InstanceManager', 'InstanceGroupManager', 'DeferJobCreatedManager', 'UUID_DEFAULT']
logger = logging.getLogger('awx.main.managers')
UUID_DEFAULT = '00000000-0000-0000-0000-000000000000'
class DeferJobCreatedManager(models.Manager):
@@ -104,20 +106,17 @@ class InstanceManager(models.Manager):
"""Return the currently active instance."""
# If we are running unit tests, return a stub record.
if settings.IS_TESTING(sys.argv) or hasattr(sys, '_called_from_test'):
return self.model(id=1, hostname='localhost', uuid='00000000-0000-0000-0000-000000000000')
return self.model(id=1, hostname=settings.CLUSTER_HOST_ID, uuid=UUID_DEFAULT)
node = self.filter(hostname=settings.CLUSTER_HOST_ID)
if node.exists():
return node[0]
raise RuntimeError("No instance found with the current cluster host id")
def register(self, uuid=None, hostname=None, ip_address=None, node_type=None):
if not uuid:
uuid = settings.SYSTEM_UUID
def register(self, uuid=None, hostname=None, ip_address=None, node_type='hybrid', defaults=None):
if not hostname:
hostname = settings.CLUSTER_HOST_ID
if not node_type:
node_type = "hybrid"
with advisory_lock('instance_registration_%s' % hostname):
if settings.AWX_AUTO_DEPROVISION_INSTANCES:
# detect any instances with the same IP address.
@@ -130,13 +129,25 @@ class InstanceManager(models.Manager):
other_inst.save(update_fields=['ip_address'])
logger.warning("IP address {0} conflict detected, ip address unset for host {1}.".format(ip_address, other_hostname))
instance = self.filter(hostname=hostname)
# Return existing instance that matches hostname or UUID (default to UUID)
if uuid is not None and uuid != UUID_DEFAULT and self.filter(uuid=uuid).exists():
instance = self.filter(uuid=uuid)
else:
# if instance was not retrieved by uuid and hostname was, use the hostname
instance = self.filter(hostname=hostname)
# Return existing instance
if instance.exists():
instance = instance.get()
instance = instance.first() # in the unusual occasion that there is more than one, only get one
update_fields = []
# if instance was retrieved by uuid and hostname has changed, update hostname
if instance.hostname != hostname:
logger.warning("passed in hostname {0} is different from the original hostname {1}, updating to {0}".format(hostname, instance.hostname))
instance.hostname = hostname
update_fields.append('hostname')
# if any other fields are to be updated
if instance.ip_address != ip_address:
instance.ip_address = ip_address
update_fields.append('ip_address')
if instance.node_type != node_type:
instance.node_type = node_type
update_fields.append('node_type')
@@ -145,7 +156,17 @@ class InstanceManager(models.Manager):
return (True, instance)
else:
return (False, instance)
instance = self.create(uuid=uuid, hostname=hostname, ip_address=ip_address, capacity=0, node_type=node_type)
# Create new instance, and fill in default values
create_defaults = dict(capacity=0)
if defaults is not None:
create_defaults.update(defaults)
uuid_option = {}
if uuid is not None:
uuid_option = dict(uuid=uuid)
if node_type == 'execution' and 'version' not in create_defaults:
create_defaults['version'] = RECEPTOR_PENDING
instance = self.create(hostname=hostname, ip_address=ip_address, node_type=node_type, **create_defaults, **uuid_option)
return (True, instance)
def get_or_register(self):
@@ -153,17 +174,18 @@ class InstanceManager(models.Manager):
from awx.main.management.commands.register_queue import RegisterQueue
pod_ip = os.environ.get('MY_POD_IP')
registered = self.register(ip_address=pod_ip)
if settings.IS_K8S:
registered = self.register(ip_address=pod_ip, node_type='control', uuid=settings.SYSTEM_UUID)
else:
registered = self.register(ip_address=pod_ip, uuid=settings.SYSTEM_UUID)
RegisterQueue(settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME, 100, 0, [], is_container_group=False).register()
RegisterQueue(settings.DEFAULT_EXECUTION_QUEUE_NAME, 100, 0, [], is_container_group=True).register()
RegisterQueue(
settings.DEFAULT_EXECUTION_QUEUE_NAME, 100, 0, [], is_container_group=True, pod_spec_override=settings.DEFAULT_EXECUTION_QUEUE_POD_SPEC_OVERRIDE
).register()
return registered
else:
return (False, self.me())
def active_count(self):
"""Return count of active Tower nodes for licensing."""
return self.all().count()
class InstanceGroupManager(models.Manager):
"""A custom manager class for the Instance model.
@@ -197,6 +219,8 @@ class InstanceGroupManager(models.Manager):
if name not in graph:
graph[name] = {}
graph[name]['consumed_capacity'] = 0
for capacity_type in ('execution', 'control'):
graph[name][f'consumed_{capacity_type}_capacity'] = 0
if breakdown:
graph[name]['committed_capacity'] = 0
graph[name]['running_capacity'] = 0
@@ -232,6 +256,8 @@ class InstanceGroupManager(models.Manager):
if group_name not in graph:
self.zero_out_group(graph, group_name, breakdown)
graph[group_name]['consumed_capacity'] += impact
capacity_type = get_capacity_type(t)
graph[group_name][f'consumed_{capacity_type}_capacity'] += impact
if breakdown:
graph[group_name]['committed_capacity'] += impact
elif t.status == 'running':
@@ -249,6 +275,8 @@ class InstanceGroupManager(models.Manager):
if group_name not in graph:
self.zero_out_group(graph, group_name, breakdown)
graph[group_name]['consumed_capacity'] += impact
capacity_type = get_capacity_type(t)
graph[group_name][f'consumed_{capacity_type}_capacity'] += impact
if breakdown:
graph[group_name]['running_capacity'] += impact
else:

View File

@@ -9,12 +9,6 @@ def remove_iso_instances(apps, schema_editor):
Instance.objects.filter(rampart_groups__controller__isnull=False).delete()
def remove_iso_groups(apps, schema_editor):
InstanceGroup = apps.get_model('main', 'InstanceGroup')
with transaction.atomic():
InstanceGroup.objects.filter(controller__isnull=False).delete()
class Migration(migrations.Migration):
atomic = False
@@ -24,7 +18,6 @@ class Migration(migrations.Migration):
operations = [
migrations.RunPython(remove_iso_instances),
migrations.RunPython(remove_iso_groups),
migrations.RemoveField(
model_name='instance',
name='last_isolated_check',

View File

@@ -0,0 +1,27 @@
# Generated by Django 2.2.20 on 2021-08-12 13:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0152_instance_node_type'),
]
operations = [
migrations.AddField(
model_name='instance',
name='last_seen',
field=models.DateTimeField(
editable=False,
help_text='Last time instance ran its heartbeat task for main cluster nodes. Last known connection to receptor mesh for execution nodes.',
null=True,
),
),
migrations.AlterField(
model_name='instance',
name='memory',
field=models.BigIntegerField(default=0, editable=False, help_text='Total system memory of this instance in bytes.'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.20 on 2021-09-01 22:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0153_instance_last_seen'),
]
operations = [
migrations.AlterField(
model_name='instance',
name='uuid',
field=models.CharField(default='00000000-0000-0000-0000-000000000000', max_length=40),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 2.2.20 on 2021-08-31 17:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0154_set_default_uuid'),
]
operations = [
migrations.AddField(
model_name='instance',
name='errors',
field=models.TextField(blank=True, default='', editable=False, help_text='Any error details from the last health check.'),
),
migrations.AddField(
model_name='instance',
name='last_health_check',
field=models.DateTimeField(
editable=False, help_text='Last time a health check was ran on this instance to refresh cpu, memory, and capacity.', null=True
),
),
]

View File

@@ -0,0 +1,44 @@
# Generated by Django 2.2.20 on 2021-12-17 19:26
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0155_improved_health_check'),
]
operations = [
migrations.AlterField(
model_name='instance',
name='node_type',
field=models.CharField(
choices=[
('control', 'Control plane node'),
('execution', 'Execution plane node'),
('hybrid', 'Controller and execution'),
('hop', 'Message-passing node, no execution capability'),
],
default='hybrid',
max_length=16,
),
),
migrations.CreateModel(
name='InstanceLink',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='main.Instance')),
('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reverse_peers', to='main.Instance')),
],
options={
'unique_together': {('source', 'target')},
},
),
migrations.AddField(
model_name='instance',
name='peers',
field=models.ManyToManyField(through='main.InstanceLink', to='main.Instance'),
),
]

View File

@@ -47,6 +47,7 @@ from awx.main.models.execution_environments import ExecutionEnvironment # noqa
from awx.main.models.activity_stream import ActivityStream # noqa
from awx.main.models.ha import ( # noqa
Instance,
InstanceLink,
InstanceGroup,
TowerScheduleState,
)
@@ -201,6 +202,8 @@ activity_stream_registrar.connect(Organization)
activity_stream_registrar.connect(Inventory)
activity_stream_registrar.connect(Host)
activity_stream_registrar.connect(Group)
activity_stream_registrar.connect(Instance)
activity_stream_registrar.connect(InstanceGroup)
activity_stream_registrar.connect(InventorySource)
# activity_stream_registrar.connect(InventoryUpdate)
activity_stream_registrar.connect(Credential)

View File

@@ -144,7 +144,7 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
@classmethod
def _get_task_class(cls):
from awx.main.tasks import RunAdHocCommand
from awx.main.tasks.jobs import RunAdHocCommand
return RunAdHocCommand
@@ -152,10 +152,6 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
def is_container_group_task(self):
return bool(self.instance_group and self.instance_group.is_container_group)
@property
def can_run_containerized(self):
return True
def get_absolute_url(self, request=None):
return reverse('api:ad_hoc_command_detail', kwargs={'pk': self.pk}, request=request)

View File

@@ -299,10 +299,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
def has_inputs(self, field_names=()):
for name in field_names:
if name in self.inputs:
if self.inputs[name] in ('', None):
return False
else:
if not self.has_input(name):
raise ValueError('{} is not an input field'.format(name))
return True

View File

@@ -388,7 +388,7 @@ class BasePlaybookEvent(CreatedModifiedModel):
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 import handle_success_and_failure_notifications # circular import
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])
@@ -541,8 +541,7 @@ class JobEvent(BasePlaybookEvent):
return
job = self.job
from awx.main.models import Host, JobHostSummary # circular import
from awx.main.models import Host, JobHostSummary, HostMetric
from awx.main.models import Host, JobHostSummary, HostMetric # circular import
all_hosts = Host.objects.filter(pk__in=self.host_map.values()).only('id', 'name')
existing_host_ids = set(h.id for h in all_hosts)

View File

@@ -2,6 +2,8 @@
# All Rights Reserved.
from decimal import Decimal
import random
import logging
from django.core.validators import MinValueValidator
from django.db import models, connection
@@ -16,14 +18,20 @@ from solo.models import SingletonModel
from awx import __version__ as awx_application_version
from awx.api.versioning import reverse
from awx.main.managers import InstanceManager, InstanceGroupManager
from awx.main.managers import InstanceManager, InstanceGroupManager, UUID_DEFAULT
from awx.main.fields import JSONField
from awx.main.constants import JOB_FOLDER_PREFIX
from awx.main.models.base import BaseModel, HasEditsMixin, prevent_search
from awx.main.models.unified_jobs import UnifiedJob
from awx.main.utils import get_cpu_capacity, get_mem_capacity, get_system_task_capacity
from awx.main.utils.common import get_corrected_cpu, get_cpu_effective_capacity, get_corrected_memory, get_mem_effective_capacity
from awx.main.models.mixins import RelatedJobsMixin
__all__ = ('Instance', 'InstanceGroup', 'TowerScheduleState')
# ansible-runner
from ansible_runner.utils.capacity import get_cpu_count, get_mem_in_bytes
__all__ = ('Instance', 'InstanceGroup', 'InstanceLink', 'TowerScheduleState')
logger = logging.getLogger('awx.main.models.ha')
class HasPolicyEditsMixin(HasEditsMixin):
@@ -46,12 +54,21 @@ class HasPolicyEditsMixin(HasEditsMixin):
return self._values_have_edits(new_values)
class InstanceLink(BaseModel):
source = models.ForeignKey('Instance', on_delete=models.CASCADE, related_name='+')
target = models.ForeignKey('Instance', on_delete=models.CASCADE, related_name='reverse_peers')
class Meta:
unique_together = ('source', 'target')
class Instance(HasPolicyEditsMixin, BaseModel):
"""A model representing an AWX instance running against this database."""
objects = InstanceManager()
uuid = models.CharField(max_length=40)
# Fields set in instance registration
uuid = models.CharField(max_length=40, default=UUID_DEFAULT)
hostname = models.CharField(max_length=250, unique=True)
ip_address = models.CharField(
blank=True,
@@ -60,16 +77,11 @@ class Instance(HasPolicyEditsMixin, BaseModel):
max_length=50,
unique=True,
)
# Auto-fields, implementation is different from BaseModel
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
# Fields defined in health check or heartbeat
version = models.CharField(max_length=120, blank=True)
capacity = models.PositiveIntegerField(
default=100,
editable=False,
)
capacity_adjustment = models.DecimalField(default=Decimal(1.0), max_digits=3, decimal_places=2, validators=[MinValueValidator(0)])
enabled = models.BooleanField(default=True)
managed_by_policy = models.BooleanField(default=True)
cpu = models.IntegerField(
default=0,
editable=False,
@@ -77,7 +89,33 @@ class Instance(HasPolicyEditsMixin, BaseModel):
memory = models.BigIntegerField(
default=0,
editable=False,
help_text=_('Total system memory of this instance in bytes.'),
)
errors = models.TextField(
default='',
blank=True,
editable=False,
help_text=_('Any error details from the last health check.'),
)
last_seen = models.DateTimeField(
null=True,
editable=False,
help_text=_('Last time instance ran its heartbeat task for main cluster nodes. Last known connection to receptor mesh for execution nodes.'),
)
last_health_check = models.DateTimeField(
null=True,
editable=False,
help_text=_('Last time a health check was ran on this instance to refresh cpu, memory, and capacity.'),
)
# Capacity management
capacity = models.PositiveIntegerField(
default=100,
editable=False,
)
capacity_adjustment = models.DecimalField(default=Decimal(1.0), max_digits=3, decimal_places=2, validators=[MinValueValidator(0)])
enabled = models.BooleanField(default=True)
managed_by_policy = models.BooleanField(default=True)
cpu_capacity = models.IntegerField(
default=0,
editable=False,
@@ -86,9 +124,16 @@ class Instance(HasPolicyEditsMixin, BaseModel):
default=0,
editable=False,
)
NODE_TYPE_CHOICES = [("control", "Control plane node"), ("execution", "Execution plane node"), ("hybrid", "Controller and execution")]
NODE_TYPE_CHOICES = [
("control", "Control plane node"),
("execution", "Execution plane node"),
("hybrid", "Controller and execution"),
("hop", "Message-passing node, no execution capability"),
]
node_type = models.CharField(default='hybrid', choices=NODE_TYPE_CHOICES, max_length=16)
peers = models.ManyToManyField('self', symmetrical=False, through=InstanceLink, through_fields=('source', 'target'))
class Meta:
app_label = 'main'
ordering = ("hostname",)
@@ -106,11 +151,6 @@ class Instance(HasPolicyEditsMixin, BaseModel):
def remaining_capacity(self):
return self.capacity - self.consumed_capacity
@property
def role(self):
# NOTE: TODO: Likely to repurpose this once standalone ramparts are a thing
return "awx"
@property
def jobs_running(self):
return UnifiedJob.objects.filter(
@@ -125,33 +165,117 @@ class Instance(HasPolicyEditsMixin, BaseModel):
def jobs_total(self):
return UnifiedJob.objects.filter(execution_node=self.hostname).count()
@staticmethod
def choose_online_control_plane_node():
return random.choice(
Instance.objects.filter(enabled=True, capacity__gt=0).filter(node_type__in=['control', 'hybrid']).values_list('hostname', flat=True)
)
def get_cleanup_task_kwargs(self, **kwargs):
"""
Produce options to use for the command: ansible-runner worker cleanup
returns a dict that is passed to the python interface for the runner method corresponding to that command
any kwargs will override that key=value combination in the returned dict
"""
vargs = dict()
if settings.AWX_CLEANUP_PATHS:
vargs['file_pattern'] = '/tmp/{}*'.format(JOB_FOLDER_PREFIX % '*')
vargs.update(kwargs)
if 'exclude_strings' not in vargs and vargs.get('file_pattern'):
active_pks = list(UnifiedJob.objects.filter(execution_node=self.hostname, status__in=('running', 'waiting')).values_list('pk', flat=True))
if active_pks:
vargs['exclude_strings'] = [JOB_FOLDER_PREFIX % job_id for job_id in active_pks]
if 'remove_images' in vargs or 'image_prune' in vargs:
vargs.setdefault('process_isolation_executable', 'podman')
return vargs
def is_lost(self, ref_time=None):
if self.last_seen is None:
return True
if ref_time is None:
ref_time = now()
grace_period = 120
return self.modified < ref_time - timedelta(seconds=grace_period)
grace_period = settings.CLUSTER_NODE_HEARTBEAT_PERIOD * 2
if self.node_type == 'execution':
grace_period += settings.RECEPTOR_SERVICE_ADVERTISEMENT_PERIOD
return self.last_seen < ref_time - timedelta(seconds=grace_period)
def refresh_capacity(self):
cpu = get_cpu_capacity()
mem = get_mem_capacity()
if self.enabled:
self.capacity = get_system_task_capacity(self.capacity_adjustment)
def mark_offline(self, update_last_seen=False, perform_save=True, errors=''):
if self.cpu_capacity == 0 and self.mem_capacity == 0 and self.capacity == 0 and self.errors == errors and (not update_last_seen):
return
self.cpu_capacity = self.mem_capacity = self.capacity = 0
self.errors = errors
if update_last_seen:
self.last_seen = now()
if perform_save:
update_fields = ['capacity', 'cpu_capacity', 'mem_capacity', 'errors']
if update_last_seen:
update_fields += ['last_seen']
self.save(update_fields=update_fields)
def set_capacity_value(self):
"""Sets capacity according to capacity adjustment rule (no save)"""
if self.enabled and self.node_type != 'hop':
lower_cap = min(self.mem_capacity, self.cpu_capacity)
higher_cap = max(self.mem_capacity, self.cpu_capacity)
self.capacity = lower_cap + (higher_cap - lower_cap) * self.capacity_adjustment
else:
self.capacity = 0
def refresh_capacity_fields(self):
"""Update derived capacity fields from cpu and memory (no save)"""
self.cpu_capacity = get_cpu_effective_capacity(self.cpu)
self.mem_capacity = get_mem_effective_capacity(self.memory)
self.set_capacity_value()
def save_health_data(self, version, cpu, memory, uuid=None, update_last_seen=False, errors=''):
self.last_health_check = now()
update_fields = ['last_health_check']
if update_last_seen:
self.last_seen = self.last_health_check
update_fields.append('last_seen')
if uuid is not None and self.uuid != uuid:
if self.uuid is not None:
logger.warn(f'Self-reported uuid of {self.hostname} changed from {self.uuid} to {uuid}')
self.uuid = uuid
update_fields.append('uuid')
if self.version != version:
self.version = version
update_fields.append('version')
new_cpu = get_corrected_cpu(cpu)
if new_cpu != self.cpu:
self.cpu = new_cpu
update_fields.append('cpu')
new_memory = get_corrected_memory(memory)
if new_memory != self.memory:
self.memory = new_memory
update_fields.append('memory')
if not errors:
self.refresh_capacity_fields()
self.errors = ''
else:
self.mark_offline(perform_save=False, errors=errors)
update_fields.extend(['cpu_capacity', 'mem_capacity', 'capacity', 'errors'])
self.save(update_fields=update_fields)
def local_health_check(self):
"""Only call this method on the instance that this record represents"""
errors = None
try:
# if redis is down for some reason, that means we can't persist
# playbook event data; we should consider this a zero capacity event
redis.Redis.from_url(settings.BROKER_URL).ping()
except redis.ConnectionError:
self.capacity = 0
errors = _('Failed to connect ot Redis')
self.cpu = cpu[0]
self.memory = mem[0]
self.cpu_capacity = cpu[1]
self.mem_capacity = mem[1]
self.version = awx_application_version
self.save(update_fields=['capacity', 'version', 'modified', 'cpu', 'memory', 'cpu_capacity', 'mem_capacity'])
self.save_health_data(awx_application_version, get_cpu_count(), get_mem_in_bytes(), update_last_seen=True, errors=errors)
class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin):
@@ -196,7 +320,7 @@ class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin):
@property
def capacity(self):
return sum([inst.capacity for inst in self.instances.all()])
return sum(inst.capacity for inst in self.instances.all())
@property
def jobs_running(self):
@@ -220,6 +344,8 @@ class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin):
def fit_task_to_most_remaining_capacity_instance(task, instances):
instance_most_capacity = None
for i in instances:
if i.node_type not in (task.capacity_type, 'hybrid'):
continue
if i.remaining_capacity >= task.task_impact and (
instance_most_capacity is None or i.remaining_capacity > instance_most_capacity.remaining_capacity
):
@@ -227,9 +353,11 @@ class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin):
return instance_most_capacity
@staticmethod
def find_largest_idle_instance(instances):
def find_largest_idle_instance(instances, capacity_type='execution'):
largest_instance = None
for i in instances:
if i.node_type not in (capacity_type, 'hybrid'):
continue
if i.jobs_running == 0:
if largest_instance is None:
largest_instance = i
@@ -248,7 +376,7 @@ class TowerScheduleState(SingletonModel):
def schedule_policy_task():
from awx.main.tasks import apply_cluster_membership_policies
from awx.main.tasks.system import apply_cluster_membership_policies
connection.on_commit(lambda: apply_cluster_membership_policies.apply_async())

View File

@@ -366,7 +366,7 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin):
@transaction.atomic
def schedule_deletion(self, user_id=None):
from awx.main.tasks import delete_inventory
from awx.main.tasks.system import delete_inventory
from awx.main.signals import activity_stream_delete
if self.pending_deletion is True:
@@ -382,7 +382,7 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin):
if self.kind == 'smart' and settings.AWX_REBUILD_SMART_MEMBERSHIP:
def on_commit():
from awx.main.tasks import update_host_smart_inventory_memberships
from awx.main.tasks.system import update_host_smart_inventory_memberships
update_host_smart_inventory_memberships.delay()
@@ -551,7 +551,7 @@ class Host(CommonModelNameNotUnique, RelatedJobsMixin):
if settings.AWX_REBUILD_SMART_MEMBERSHIP:
def on_commit():
from awx.main.tasks import update_host_smart_inventory_memberships
from awx.main.tasks.system import update_host_smart_inventory_memberships
update_host_smart_inventory_memberships.delay()
@@ -631,7 +631,7 @@ class Group(CommonModelNameNotUnique, RelatedJobsMixin):
@transaction.atomic
def delete_recursive(self):
from awx.main.utils import ignore_inventory_computed_fields
from awx.main.tasks import update_inventory_computed_fields
from awx.main.tasks.system import update_inventory_computed_fields
from awx.main.signals import disable_activity_stream, activity_stream_delete
def mark_actual():
@@ -1214,16 +1214,12 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin,
def is_container_group_task(self):
return bool(self.instance_group and self.instance_group.is_container_group)
@property
def can_run_containerized(self):
return True
def _get_parent_field_name(self):
return 'inventory_source'
@classmethod
def _get_task_class(cls):
from awx.main.tasks import RunInventoryUpdate
from awx.main.tasks.jobs import RunInventoryUpdate
return RunInventoryUpdate

View File

@@ -583,7 +583,7 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
@classmethod
def _get_task_class(cls):
from awx.main.tasks import RunJob
from awx.main.tasks.jobs import RunJob
return RunJob
@@ -743,10 +743,6 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
return "$hidden due to Ansible no_log flag$"
return artifacts
@property
def can_run_containerized(self):
return True
@property
def is_container_group_task(self):
return bool(self.instance_group and self.instance_group.is_container_group)
@@ -1217,7 +1213,7 @@ class SystemJob(UnifiedJob, SystemJobOptions, JobNotificationMixin):
@classmethod
def _get_task_class(cls):
from awx.main.tasks import RunSystemJob
from awx.main.tasks.jobs import RunSystemJob
return RunSystemJob
@@ -1236,10 +1232,6 @@ class SystemJob(UnifiedJob, SystemJobOptions, JobNotificationMixin):
return UnpartitionedSystemJobEvent
return SystemJobEvent
@property
def can_run_on_control_plane(self):
return True
@property
def task_impact(self):
return 5

View File

@@ -508,7 +508,7 @@ class JobNotificationMixin(object):
return (msg, body)
def send_notification_templates(self, status):
from awx.main.tasks import send_notifications # avoid circular import
from awx.main.tasks.system import send_notifications # avoid circular import
if status not in ['running', 'succeeded', 'failed']:
raise ValueError(_("status must be either running, succeeded or failed"))

View File

@@ -118,7 +118,7 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi
from awx.main.models import Credential
public_galaxy_credential = Credential.objects.filter(managed=True, name='Ansible Galaxy').first()
if public_galaxy_credential not in self.galaxy_credentials.all():
if public_galaxy_credential is not None and public_galaxy_credential not in self.galaxy_credentials.all():
self.galaxy_credentials.add(public_galaxy_credential)

View File

@@ -471,7 +471,7 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
r = super(Project, self).delete(*args, **kwargs)
for path_to_delete in paths_to_delete:
if self.scm_type and path_to_delete: # non-manual, concrete path
from awx.main.tasks import delete_project_files
from awx.main.tasks.system import delete_project_files
delete_project_files.delay(path_to_delete)
return r
@@ -532,7 +532,7 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin, TaskManage
@classmethod
def _get_task_class(cls):
from awx.main.tasks import RunProjectUpdate
from awx.main.tasks.jobs import RunProjectUpdate
return RunProjectUpdate
@@ -553,10 +553,6 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin, TaskManage
websocket_data.update(dict(project_id=self.project.id))
return websocket_data
@property
def can_run_on_control_plane(self):
return True
@property
def event_class(self):
if self.has_unpartitioned_events:
@@ -619,16 +615,22 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin, TaskManage
@property
def preferred_instance_groups(self):
'''
Project updates should pretty much always run on the control plane
however, we are not yet saying no to custom groupings within the control plane
Thus, we return custom groups and then unconditionally add the control plane
'''
if self.organization is not None:
organization_groups = [x for x in self.organization.instance_groups.all()]
else:
organization_groups = []
template_groups = [x for x in super(ProjectUpdate, self).preferred_instance_groups]
selected_groups = template_groups + organization_groups
if not any([not group.is_container_group for group in selected_groups]):
selected_groups = selected_groups + list(self.control_plane_instance_group)
if not selected_groups:
return self.global_instance_groups
controlplane_ig = self.control_plane_instance_group
if controlplane_ig and controlplane_ig[0] and controlplane_ig[0] not in selected_groups:
selected_groups += controlplane_ig
return selected_groups
def save(self, *args, **kwargs):

View File

@@ -36,21 +36,21 @@ from awx.main.dispatch import get_local_queuename
from awx.main.dispatch.control import Control as ControlDispatcher
from awx.main.registrar import activity_stream_registrar
from awx.main.models.mixins import ResourceMixin, TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin
from awx.main.utils import (
from awx.main.utils.common import (
camelcase_to_underscore,
get_model_for_type,
encrypt_dict,
decrypt_field,
_inventory_updates,
copy_model_by_class,
copy_m2m_relationships,
get_type_for_model,
parse_yaml_or_json,
getattr_dne,
polymorphic,
schedule_task_manager,
get_event_partition_epoch,
get_capacity_type,
)
from awx.main.utils.encryption import encrypt_dict, decrypt_field
from awx.main.utils import polymorphic
from awx.main.constants import ACTIVE_STATES, CAN_CANCEL
from awx.main.redact import UriCleaner, REPLACE_STR
from awx.main.consumers import emit_channel_notification
@@ -740,15 +740,8 @@ class UnifiedJob(
raise NotImplementedError # Implement in subclasses.
@property
def can_run_on_control_plane(self):
if settings.IS_K8S:
return False
return True
@property
def can_run_containerized(self):
return False
def capacity_type(self):
return get_capacity_type(self)
def _get_parent_field_name(self):
return 'unified_job_template' # Override in subclasses.
@@ -1053,7 +1046,7 @@ class UnifiedJob(
fd = tempfile.NamedTemporaryFile(
mode='w', prefix='{}-{}-'.format(self.model_to_str(), self.pk), suffix='.out', dir=settings.JOBOUTPUT_ROOT, encoding='utf-8'
)
from awx.main.tasks import purge_old_stdout_files # circular import
from awx.main.tasks.system import purge_old_stdout_files # circular import
purge_old_stdout_files.apply_async()
@@ -1442,9 +1435,13 @@ class UnifiedJob(
if not settings.IS_K8S:
default_instance_group_names.append(settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME)
default_instance_groups = InstanceGroup.objects.filter(name__in=default_instance_group_names)
default_instance_groups = list(InstanceGroup.objects.filter(name__in=default_instance_group_names))
return list(default_instance_groups)
# assure deterministic precedence by making sure the default group is first
if (not settings.IS_K8S) and default_instance_groups and default_instance_groups[0].name != settings.DEFAULT_EXECUTION_QUEUE_NAME:
default_instance_groups.reverse()
return default_instance_groups
def awx_meta_vars(self):
"""
@@ -1500,7 +1497,12 @@ class UnifiedJob(
return False
def log_lifecycle(self, state, blocked_by=None):
extra = {'type': self._meta.model_name, 'task_id': self.id, 'state': state}
extra = {
'type': self._meta.model_name,
'task_id': self.id,
'state': state,
'work_unit_id': self.work_unit_id,
}
if self.unified_job_template:
extra["template_name"] = self.unified_job_template.name
if state == "blocked" and blocked_by:
@@ -1509,6 +1511,11 @@ class UnifiedJob(
extra["blocked_by"] = blocked_by_msg
else:
msg = f"{self._meta.model_name}-{self.id} {state.replace('_', ' ')}"
if state == "controller_node_chosen":
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)
@property

View File

@@ -813,7 +813,7 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin):
return True
def send_approval_notification(self, approval_status):
from awx.main.tasks import send_notifications # avoid circular import
from awx.main.tasks.system import send_notifications # avoid circular import
if self.workflow_job_template is None:
return

View File

@@ -9,6 +9,7 @@ from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _
from awx.main.notifications.base import AWXBaseEmailBackend
from awx.main.utils import get_awx_http_client_headers
from awx.main.notifications.custom_notification_base import CustomNotificationBase
logger = logging.getLogger('awx.main.notifications.rocketchat_backend')
@@ -38,7 +39,9 @@ class RocketChatBackend(AWXBaseEmailBackend, CustomNotificationBase):
if optvalue is not None:
payload[optval] = optvalue.strip()
r = requests.post("{}".format(m.recipients()[0]), data=json.dumps(payload), verify=(not self.rocketchat_no_verify_ssl))
r = requests.post(
"{}".format(m.recipients()[0]), data=json.dumps(payload), headers=get_awx_http_client_headers(), verify=(not self.rocketchat_no_verify_ssl)
)
if r.status_code >= 400:
logger.error(smart_text(_("Error sending notification rocket.chat: {}").format(r.status_code)))

View File

@@ -2,7 +2,8 @@
# All Rights Reserved.
import logging
from slackclient import SlackClient
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _
@@ -28,23 +29,30 @@ class SlackBackend(AWXBaseEmailBackend, CustomNotificationBase):
self.color = hex_color
def send_messages(self, messages):
connection = SlackClient(self.token)
client = WebClient(self.token)
sent_messages = 0
for m in messages:
try:
for r in m.recipients():
if r.startswith('#'):
r = r[1:]
thread = None
channel = r
thread = None
if ',' in r:
channel, thread = r.split(',')
if self.color:
ret = connection.api_call("chat.postMessage", channel=r, as_user=True, attachments=[{"color": self.color, "text": m.subject}])
response = client.chat_postMessage(
channel=channel, thread_ts=thread, as_user=True, attachments=[{"color": self.color, "text": m.subject}]
)
else:
ret = connection.api_call("chat.postMessage", channel=r, as_user=True, text=m.subject)
logger.debug(ret)
if ret['ok']:
response = client.chat_postMessage(channel=channel, thread_ts=thread, as_user=True, text=m.subject)
logger.debug(response)
if response['ok']:
sent_messages += 1
else:
raise RuntimeError("Slack Notification unable to send {}: {} ({})".format(r, m.subject, ret['error']))
except Exception as e:
raise RuntimeError("Slack Notification unable to send {}: {} ({})".format(r, m.subject, response['error']))
except SlackApiError as e:
logger.error(smart_text(_("Exception sending messages: {}").format(e)))
if not self.fail_silently:
raise

View File

@@ -9,29 +9,12 @@ from kubernetes import client, config
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from awx.main.utils.common import parse_yaml_or_json
from awx.main.utils.common import parse_yaml_or_json, deepmerge
from awx.main.utils.execution_environments import get_default_pod_spec
logger = logging.getLogger('awx.main.scheduler')
def deepmerge(a, b):
"""
Merge dict structures and return the result.
>>> a = {'first': {'all_rows': {'pass': 'dog', 'number': '1'}}}
>>> b = {'first': {'all_rows': {'fail': 'cat', 'number': '5'}}}
>>> import pprint; pprint.pprint(deepmerge(a, b))
{'first': {'all_rows': {'fail': 'cat', 'number': '5', 'pass': 'dog'}}}
"""
if isinstance(a, dict) and isinstance(b, dict):
return dict([(k, deepmerge(a.get(k), b.get(k))) for k in set(a.keys()).union(b.keys())])
elif b is None:
return a
else:
return b
class PodManager(object):
def __init__(self, task=None):
self.task = task
@@ -183,7 +166,7 @@ class PodManager(object):
pod_spec_override = {}
if self.task and self.task.instance_group.pod_spec_override:
pod_spec_override = parse_yaml_or_json(self.task.instance_group.pod_spec_override)
pod_spec = {**default_pod_spec, **pod_spec_override}
pod_spec = deepmerge(default_pod_spec, pod_spec_override)
if self.task:
pod_spec['metadata'] = deepmerge(

View File

@@ -13,7 +13,6 @@ from django.db import transaction, connection
from django.utils.translation import ugettext_lazy as _, gettext_noop
from django.utils.timezone import now as tz_now
from django.conf import settings
from django.db.models import Q
# AWX
from awx.main.dispatch.reaper import reap_job
@@ -69,12 +68,13 @@ class TaskManager:
"""
Init AFTER we know this instance of the task manager will run because the lock is acquired.
"""
instances = Instance.objects.filter(~Q(hostname=None), enabled=True)
instances = Instance.objects.filter(hostname__isnull=False, enabled=True).exclude(node_type='hop')
self.real_instances = {i.hostname: i for i in instances}
instances_partial = [
SimpleNamespace(
obj=instance,
node_type=instance.node_type,
remaining_capacity=instance.remaining_capacity,
capacity=instance.capacity,
jobs_running=instance.jobs_running,
@@ -86,7 +86,21 @@ class TaskManager:
instances_by_hostname = {i.hostname: i for i in instances_partial}
for rampart_group in InstanceGroup.objects.prefetch_related('instances'):
self.graph[rampart_group.name] = dict(graph=DependencyGraph(), capacity_total=rampart_group.capacity, consumed_capacity=0, instances=[])
self.graph[rampart_group.name] = dict(
graph=DependencyGraph(),
execution_capacity=0,
control_capacity=0,
consumed_capacity=0,
consumed_control_capacity=0,
consumed_execution_capacity=0,
instances=[],
)
for instance in rampart_group.instances.all():
if not instance.enabled:
continue
for capacity_type in ('control', 'execution'):
if instance.node_type in (capacity_type, 'hybrid'):
self.graph[rampart_group.name][f'{capacity_type}_capacity'] += instance.capacity
for instance in rampart_group.instances.filter(enabled=True).order_by('hostname'):
if instance.hostname in instances_by_hostname:
self.graph[rampart_group.name]['instances'].append(instances_by_hostname[instance.hostname])
@@ -224,7 +238,7 @@ class TaskManager:
update_fields = ['status', 'start_args']
workflow_job.status = new_status
if reason:
logger.info(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
@@ -243,7 +257,7 @@ class TaskManager:
if self.start_task_limit == 0:
# schedule another run immediately after this task manager
schedule_task_manager()
from awx.main.tasks import handle_work_error, handle_work_success
from awx.main.tasks.system import handle_work_error, handle_work_success
dependent_tasks = dependent_tasks or []
@@ -270,35 +284,45 @@ class TaskManager:
logger.debug('Transitioning %s to running status.', task.log_format)
schedule_task_manager()
elif rampart_group.is_container_group:
# find one real, non-containerized instance with capacity to
# act as the controller for k8s API interaction
match = None
for group in InstanceGroup.objects.filter(is_container_group=False):
match = group.fit_task_to_most_remaining_capacity_instance(task, group.instances.all())
if match:
break
task.instance_group = rampart_group
if match is None:
logger.warn('No available capacity to run containerized <{}>.'.format(task.log_format))
elif task.can_run_containerized and any(ig.is_container_group for ig in task.preferred_instance_groups):
task.controller_node = match.hostname
if task.capacity_type == 'execution':
# find one real, non-containerized instance with capacity to
# act as the controller for k8s API interaction
try:
task.controller_node = Instance.choose_online_control_plane_node()
task.log_lifecycle("controller_node_chosen")
except IndexError:
logger.warning("No control plane nodes available to run containerized job {}".format(task.log_format))
return
else:
# project updates and inventory updates don't *actually* run in pods, so
# project updates and system jobs don't *actually* run in pods, so
# just pick *any* non-containerized host and use it as the execution node
task.execution_node = match.hostname
task.execution_node = Instance.choose_online_control_plane_node()
task.log_lifecycle("execution_node_chosen")
logger.debug('Submitting containerized {} to queue {}.'.format(task.log_format, task.execution_node))
else:
task.instance_group = rampart_group
if instance is not None:
task.execution_node = instance.hostname
logger.debug('Submitting {} to <instance group, instance> <{},{}>.'.format(task.log_format, task.instance_group_id, task.execution_node))
task.execution_node = instance.hostname
task.log_lifecycle("execution_node_chosen")
if instance.node_type == 'execution':
try:
task.controller_node = Instance.choose_online_control_plane_node()
task.log_lifecycle("controller_node_chosen")
except IndexError:
logger.warning("No control plane nodes available to manage {}".format(task.log_format))
return
else:
# control plane nodes will manage jobs locally for performance and resilience
task.controller_node = task.execution_node
task.log_lifecycle("controller_node_chosen")
logger.debug('Submitting job {} to queue {} controlled by {}.'.format(task.log_format, task.execution_node, task.controller_node))
with disable_activity_stream():
task.celery_task_id = str(uuid.uuid4())
task.save()
task.log_lifecycle("waiting")
if rampart_group is not None:
self.consume_capacity(task, rampart_group.name)
self.consume_capacity(task, rampart_group.name, instance=instance)
def post_commit():
if task.status != 'failed' and type(task) is not WorkflowJob:
@@ -459,7 +483,7 @@ class TaskManager:
return created_dependencies
def process_pending_tasks(self, pending_tasks):
running_workflow_templates = set([wf.unified_job_template_id for wf in self.get_running_workflow_jobs()])
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:
@@ -487,24 +511,25 @@ class TaskManager:
continue
for rampart_group in preferred_instance_groups:
if task.can_run_containerized and rampart_group.is_container_group:
if task.capacity_type == 'execution' and rampart_group.is_container_group:
self.graph[rampart_group.name]['graph'].add_job(task)
self.start_task(task, rampart_group, task.get_jobs_fail_chain(), None)
found_acceptable_queue = True
break
if not task.can_run_on_control_plane:
# TODO: remove this after we have confidence that OCP control nodes are reporting node_type=control
if settings.IS_K8S and task.capacity_type == 'execution':
logger.debug("Skipping group {}, task cannot run on control plane".format(rampart_group.name))
continue
remaining_capacity = self.get_remaining_capacity(rampart_group.name)
if task.task_impact > 0 and self.get_remaining_capacity(rampart_group.name) <= 0:
remaining_capacity = self.get_remaining_capacity(rampart_group.name, capacity_type=task.capacity_type)
if task.task_impact > 0 and remaining_capacity <= 0:
logger.debug("Skipping group {}, remaining_capacity {} <= 0".format(rampart_group.name, remaining_capacity))
continue
execution_instance = InstanceGroup.fit_task_to_most_remaining_capacity_instance(
task, self.graph[rampart_group.name]['instances']
) or InstanceGroup.find_largest_idle_instance(self.graph[rampart_group.name]['instances'])
) or InstanceGroup.find_largest_idle_instance(self.graph[rampart_group.name]['instances'], capacity_type=task.capacity_type)
if execution_instance or rampart_group.is_container_group:
if not rampart_group.is_container_group:
@@ -567,7 +592,7 @@ class TaskManager:
# elsewhere
for j in UnifiedJob.objects.filter(
status__in=['pending', 'waiting', 'running'],
).exclude(execution_node__in=Instance.objects.values_list('hostname', flat=True)):
).exclude(execution_node__in=Instance.objects.exclude(node_type='hop').values_list('hostname', flat=True)):
if j.execution_node and not j.is_container_group_task:
logger.error(f'{j.execution_node} is not a registered instance; reaping {j.log_format}')
reap_job(j, 'failed')
@@ -575,16 +600,19 @@ class TaskManager:
def calculate_capacity_consumed(self, tasks):
self.graph = InstanceGroup.objects.capacity_values(tasks=tasks, graph=self.graph)
def consume_capacity(self, task, instance_group):
def consume_capacity(self, task, instance_group, instance=None):
logger.debug(
'{} consumed {} capacity units from {} with prior total of {}'.format(
task.log_format, task.task_impact, instance_group, self.graph[instance_group]['consumed_capacity']
)
)
self.graph[instance_group]['consumed_capacity'] += task.task_impact
for capacity_type in ('control', 'execution'):
if instance is None or instance.node_type in ('hybrid', capacity_type):
self.graph[instance_group][f'consumed_{capacity_type}_capacity'] += task.task_impact
def get_remaining_capacity(self, instance_group):
return self.graph[instance_group]['capacity_total'] - self.graph[instance_group]['consumed_capacity']
def get_remaining_capacity(self, instance_group, capacity_type='execution'):
return self.graph[instance_group][f'{capacity_type}_capacity'] - self.graph[instance_group][f'consumed_{capacity_type}_capacity']
def process_tasks(self, all_sorted_tasks):
running_tasks = [t for t in all_sorted_tasks if t.status in ['waiting', 'running']]

View File

@@ -34,7 +34,6 @@ from awx.main.models import (
ExecutionEnvironment,
Group,
Host,
InstanceGroup,
Inventory,
InventorySource,
Job,
@@ -58,7 +57,7 @@ from awx.main.models import (
from awx.main.constants import CENSOR_VALUE
from awx.main.utils import model_instance_diff, model_to_dict, camelcase_to_underscore, get_current_apps
from awx.main.utils import ignore_inventory_computed_fields, ignore_inventory_group_removal, _inventory_updates
from awx.main.tasks import update_inventory_computed_fields
from awx.main.tasks.system import update_inventory_computed_fields, handle_removed_image
from awx.main.fields import (
is_implicit_parent,
update_role_parentage_for_instance,
@@ -377,6 +376,7 @@ def model_serializer_mapping():
models.Inventory: serializers.InventorySerializer,
models.Host: serializers.HostSerializer,
models.Group: serializers.GroupSerializer,
models.Instance: serializers.InstanceSerializer,
models.InstanceGroup: serializers.InstanceGroupSerializer,
models.InventorySource: serializers.InventorySourceSerializer,
models.Credential: serializers.CredentialSerializer,
@@ -624,10 +624,26 @@ def deny_orphaned_approvals(sender, instance, **kwargs):
approval.deny()
def _handle_image_cleanup(removed_image, pk):
if (not removed_image) or ExecutionEnvironment.objects.filter(image=removed_image).exclude(pk=pk).exists():
return # if other EE objects reference the tag, then do not purge it
handle_removed_image.delay(remove_images=[removed_image])
@receiver(pre_delete, sender=ExecutionEnvironment)
def remove_default_ee(sender, instance, **kwargs):
if instance.id == getattr(settings.DEFAULT_EXECUTION_ENVIRONMENT, 'id', None):
settings.DEFAULT_EXECUTION_ENVIRONMENT = None
_handle_image_cleanup(instance.image, instance.pk)
@receiver(post_save, sender=ExecutionEnvironment)
def remove_stale_image(sender, instance, created, **kwargs):
if created:
return
removed_image = instance._prior_values_store.get('image')
if removed_image and removed_image != instance.image:
_handle_image_cleanup(removed_image, instance.pk)
@receiver(post_save, sender=Session)
@@ -659,9 +675,3 @@ def create_access_token_user_if_missing(sender, **kwargs):
post_save.disconnect(create_access_token_user_if_missing, sender=OAuth2AccessToken)
obj.save()
post_save.connect(create_access_token_user_if_missing, sender=OAuth2AccessToken)
# Connect the Instance Group to Activity Stream receivers.
post_save.connect(activity_stream_create, sender=InstanceGroup, dispatch_uid=str(InstanceGroup) + "_create")
pre_save.connect(activity_stream_update, sender=InstanceGroup, dispatch_uid=str(InstanceGroup) + "_update")
pre_delete.connect(activity_stream_delete, sender=InstanceGroup, dispatch_uid=str(InstanceGroup) + "_delete")

View File

File diff suppressed because it is too large Load Diff

542
awx/main/tasks/receptor.py Normal file
View File

@@ -0,0 +1,542 @@
# Python
from base64 import b64encode
from collections import namedtuple
import concurrent.futures
from enum import Enum
import logging
import os
import shutil
import socket
import sys
import threading
import time
import yaml
# Django
from django.conf import settings
# Runner
import ansible_runner
# AWX
from awx.main.utils.execution_environments import get_default_pod_spec
from awx.main.exceptions import ReceptorNodeNotFound
from awx.main.utils.common import (
deepmerge,
parse_yaml_or_json,
cleanup_new_process,
)
# Receptorctl
from receptorctl.socket_interface import ReceptorControl
logger = logging.getLogger('awx.main.tasks.receptor')
__RECEPTOR_CONF = '/etc/receptor/receptor.conf'
RECEPTOR_ACTIVE_STATES = ('Pending', 'Running')
class ReceptorConnectionType(Enum):
DATAGRAM = 0
STREAM = 1
STREAMTLS = 2
def get_receptor_sockfile():
with open(__RECEPTOR_CONF, 'r') as f:
data = yaml.safe_load(f)
for section in data:
for entry_name, entry_data in section.items():
if entry_name == 'control-service':
if 'filename' in entry_data:
return entry_data['filename']
else:
raise RuntimeError(f'Receptor conf {__RECEPTOR_CONF} control-service entry does not have a filename parameter')
else:
raise RuntimeError(f'Receptor conf {__RECEPTOR_CONF} does not have control-service entry needed to get sockfile')
def get_tls_client(use_stream_tls=None):
if not use_stream_tls:
return None
with open(__RECEPTOR_CONF, 'r') as f:
data = yaml.safe_load(f)
for section in data:
for entry_name, entry_data in section.items():
if entry_name == 'tls-client':
if 'name' in entry_data:
return entry_data['name']
return None
def get_receptor_ctl():
receptor_sockfile = get_receptor_sockfile()
try:
return ReceptorControl(receptor_sockfile, config=__RECEPTOR_CONF, tlsclient=get_tls_client(True))
except RuntimeError:
return ReceptorControl(receptor_sockfile)
def get_conn_type(node_name, receptor_ctl):
all_nodes = receptor_ctl.simple_command("status").get('Advertisements', None)
for node in all_nodes:
if node.get('NodeID') == node_name:
return ReceptorConnectionType(node.get('ConnType'))
raise ReceptorNodeNotFound(f'Instance {node_name} is not in the receptor mesh')
def administrative_workunit_reaper(work_list=None):
"""
This releases completed work units that were spawned by actions inside of this module
specifically, this should catch any completed work unit left by
- worker_info
- worker_cleanup
These should ordinarily be released when the method finishes, but this is a
cleanup of last-resort, in case something went awry
"""
receptor_ctl = get_receptor_ctl()
if work_list is None:
work_list = receptor_ctl.simple_command("work list")
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'):
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}')
receptor_ctl.simple_command(f"work release {unit_id}")
class RemoteJobError(RuntimeError):
pass
def run_until_complete(node, timing_data=None, **kwargs):
"""
Runs an ansible-runner work_type on remote node, waits until it completes, then returns stdout.
"""
receptor_ctl = get_receptor_ctl()
use_stream_tls = getattr(get_conn_type(node, receptor_ctl), 'name', None) == "STREAMTLS"
kwargs.setdefault('tlsclient', get_tls_client(use_stream_tls))
kwargs.setdefault('ttl', '20s')
kwargs.setdefault('payload', '')
transmit_start = time.time()
sign_work = False if settings.IS_K8S else True
result = receptor_ctl.submit_work(worktype='ansible-runner', node=node, signwork=sign_work, **kwargs)
unit_id = result['unitid']
run_start = time.time()
if timing_data:
timing_data['transmit_timing'] = run_start - transmit_start
run_timing = 0.0
stdout = ''
try:
resultfile = receptor_ctl.get_work_results(unit_id)
while run_timing < 20.0:
status = receptor_ctl.simple_command(f'work status {unit_id}')
state_name = status.get('StateName')
if state_name not in RECEPTOR_ACTIVE_STATES:
break
run_timing = time.time() - run_start
time.sleep(0.5)
else:
raise RemoteJobError(f'Receptor job timeout on {node} after {run_timing} seconds, state remains in {state_name}')
if timing_data:
timing_data['run_timing'] = run_timing
stdout = resultfile.read()
stdout = str(stdout, encoding='utf-8')
finally:
if settings.RECEPTOR_RELEASE_WORK:
res = receptor_ctl.simple_command(f"work release {unit_id}")
if res != {'released': unit_id}:
logger.warn(f'Could not confirm release of receptor work unit id {unit_id} from {node}, data: {res}')
receptor_ctl.close()
if state_name.lower() == 'failed':
work_detail = status.get('Detail', '')
if work_detail:
raise RemoteJobError(f'Receptor error from {node}, detail:\n{work_detail}')
else:
raise RemoteJobError(f'Unknown ansible-runner error on node {node}, stdout:\n{stdout}')
return stdout
def worker_info(node_name, work_type='ansible-runner'):
error_list = []
data = {'errors': error_list, 'transmit_timing': 0.0}
try:
stdout = run_until_complete(node=node_name, timing_data=data, params={"params": "--worker-info"})
yaml_stdout = stdout.strip()
remote_data = {}
try:
remote_data = yaml.safe_load(yaml_stdout)
except Exception as json_e:
error_list.append(f'Failed to parse node {node_name} --worker-info output as YAML, error: {json_e}, data:\n{yaml_stdout}')
if not isinstance(remote_data, dict):
error_list.append(f'Remote node {node_name} --worker-info output is not a YAML dict, output:{stdout}')
else:
error_list.extend(remote_data.pop('errors', [])) # merge both error lists
data.update(remote_data)
except RemoteJobError as exc:
details = exc.args[0]
if 'unrecognized arguments: --worker-info' in details:
error_list.append(f'Old version (2.0.1 or earlier) of ansible-runner on node {node_name} without --worker-info')
else:
error_list.append(details)
except (ReceptorNodeNotFound, RuntimeError) as exc:
error_list.append(str(exc))
# If we have a connection error, missing keys would be trivial consequence of that
if not data['errors']:
# see tasks.py usage of keys
missing_keys = set(('runner_version', 'mem_in_bytes', 'cpu_count')) - set(data.keys())
if missing_keys:
data['errors'].append('Worker failed to return keys {}'.format(' '.join(missing_keys)))
return data
def _convert_args_to_cli(vargs):
"""
For the ansible-runner worker cleanup command
converts the dictionary (parsed argparse variables) used for python interface
into a string of CLI options, which has to be used on execution nodes.
"""
args = ['cleanup']
for option in ('exclude_strings', 'remove_images'):
if vargs.get(option):
args.append('--{}={}'.format(option.replace('_', '-'), ' '.join(vargs.get(option))))
for option in ('file_pattern', 'image_prune', 'process_isolation_executable', 'grace_period'):
if vargs.get(option) is True:
args.append('--{}'.format(option.replace('_', '-')))
elif vargs.get(option) not in (None, ''):
args.append('--{}={}'.format(option.replace('_', '-'), vargs.get(option)))
return args
def worker_cleanup(node_name, vargs, timeout=300.0):
args = _convert_args_to_cli(vargs)
remote_command = ' '.join(args)
logger.debug(f'Running command over receptor mesh on {node_name}: ansible-runner worker {remote_command}')
stdout = run_until_complete(node=node_name, params={"params": remote_command})
return stdout
class TransmitterThread(threading.Thread):
def run(self):
self.exc = None
try:
super().run()
except Exception:
self.exc = sys.exc_info()
class AWXReceptorJob:
def __init__(self, task, runner_params=None):
self.task = task
self.runner_params = runner_params
self.unit_id = None
if self.task and not self.task.instance.is_container_group_task:
execution_environment_params = self.task.build_execution_environment_params(self.task.instance, runner_params['private_data_dir'])
self.runner_params.update(execution_environment_params)
if not settings.IS_K8S and self.work_type == 'local' and 'only_transmit_kwargs' not in self.runner_params:
self.runner_params['only_transmit_kwargs'] = True
def run(self):
# We establish a connection to the Receptor socket
receptor_ctl = get_receptor_ctl()
res = None
try:
res = self._run_internal(receptor_ctl)
return res
finally:
# Make sure to always release the work unit if we established it
if self.unit_id is not None and settings.RECEPTOR_RELEASE_WORK:
try:
receptor_ctl.simple_command(f"work release {self.unit_id}")
except Exception:
logger.exception(f"Error releasing work unit {self.unit_id}.")
@property
def sign_work(self):
return False if settings.IS_K8S else True
def _run_internal(self, receptor_ctl):
# Create a socketpair. Where the left side will be used for writing our payload
# (private data dir, kwargs). The right side will be passed to Receptor for
# reading.
sockin, sockout = socket.socketpair()
transmitter_thread = TransmitterThread(target=self.transmit, args=[sockin])
transmitter_thread.start()
# submit our work, passing
# in the right side of our socketpair for reading.
_kw = {}
if self.work_type == 'ansible-runner':
_kw['node'] = self.task.instance.execution_node
use_stream_tls = get_conn_type(_kw['node'], receptor_ctl).name == "STREAMTLS"
_kw['tlsclient'] = get_tls_client(use_stream_tls)
result = receptor_ctl.submit_work(worktype=self.work_type, payload=sockout.makefile('rb'), params=self.receptor_params, signwork=self.sign_work, **_kw)
self.unit_id = result['unitid']
# Update the job with the work unit in-memory so that the log_lifecycle
# will print out the work unit that is to be associated with the job in the database
# via the update_model() call.
# We want to log the work_unit_id as early as possible. A failure can happen in between
# when we start the job in receptor and when we associate the job <-> work_unit_id.
# In that case, there will be work running in receptor and Controller will not know
# which Job it is associated with.
# We do not programatically handle this case. Ideally, we would handle this with a reaper case.
# The two distinct job lifecycle log events below allow for us to at least detect when this
# edge case occurs. If the lifecycle event work_unit_id_received occurs without the
# work_unit_id_assigned event then this case may have occured.
self.task.instance.work_unit_id = result['unitid'] # Set work_unit_id in-memory only
self.task.instance.log_lifecycle("work_unit_id_received")
self.task.update_model(self.task.instance.pk, work_unit_id=result['unitid'])
self.task.instance.log_lifecycle("work_unit_id_assigned")
sockin.close()
sockout.close()
if transmitter_thread.exc:
raise transmitter_thread.exc[1].with_traceback(transmitter_thread.exc[2])
transmitter_thread.join()
# Artifacts are an output, but sometimes they are an input as well
# this is the case with fact cache, where clearing facts deletes a file, and this must be captured
artifact_dir = os.path.join(self.runner_params['private_data_dir'], 'artifacts')
if os.path.exists(artifact_dir):
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':
receptor_ctl.simple_command(f"work cancel {self.unit_id}")
resultsock.shutdown(socket.SHUT_RDWR)
resultfile.close()
elif res.status == 'error':
try:
unit_status = receptor_ctl.simple_command(f'work status {self.unit_id}')
detail = unit_status.get('Detail', None)
state_name = unit_status.get('StateName', None)
except Exception:
detail = ''
state_name = ''
logger.exception(f'An error was encountered while getting status for work unit {self.unit_id}')
if 'exceeded quota' in detail:
logger.warn(detail)
log_name = self.task.instance.log_format
logger.warn(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
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.warn(f'No result details or output from {self.task.instance.log_format}, status:\n{state_name}')
except Exception:
raise RuntimeError(detail)
return res
# Spawned in a thread so Receptor can start reading before we finish writing, we
# write our payload to the left side of our socketpair.
@cleanup_new_process
def transmit(self, _socket):
try:
ansible_runner.interface.run(streamer='transmit', _output=_socket.makefile('wb'), **self.runner_params)
finally:
# Socket must be shutdown here, or the reader will hang forever.
_socket.shutdown(socket.SHUT_WR)
@cleanup_new_process
def processor(self, resultfile):
return ansible_runner.interface.run(
streamer='process',
quiet=True,
_input=resultfile,
event_handler=self.task.event_handler,
finished_callback=self.task.finished_callback,
status_handler=self.task.status_handler,
**self.runner_params,
)
@property
def receptor_params(self):
if self.task.instance.is_container_group_task:
spec_yaml = yaml.dump(self.pod_definition, explicit_start=True)
receptor_params = {
"secret_kube_pod": spec_yaml,
"pod_pending_timeout": getattr(settings, 'AWX_CONTAINER_GROUP_POD_PENDING_TIMEOUT', "5m"),
}
if self.credential:
kubeconfig_yaml = yaml.dump(self.kube_config, explicit_start=True)
receptor_params["secret_kube_config"] = kubeconfig_yaml
else:
private_data_dir = self.runner_params['private_data_dir']
if self.work_type == 'ansible-runner' and settings.AWX_CLEANUP_PATHS:
# on execution nodes, we rely on the private data dir being deleted
cli_params = f"--private-data-dir={private_data_dir} --delete"
else:
# on hybrid nodes, we rely on the private data dir NOT being deleted
cli_params = f"--private-data-dir={private_data_dir}"
receptor_params = {"params": cli_params}
return receptor_params
@property
def work_type(self):
if self.task.instance.is_container_group_task:
if self.credential:
return 'kubernetes-runtime-auth'
return 'kubernetes-incluster-auth'
if self.task.instance.execution_node == settings.CLUSTER_HOST_ID or self.task.instance.execution_node == self.task.instance.controller_node:
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.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
default_pod_spec = get_default_pod_spec()
pod_spec_override = {}
if self.task and self.task.instance.instance_group.pod_spec_override:
pod_spec_override = parse_yaml_or_json(self.task.instance.instance_group.pod_spec_override)
# According to the deepmerge docstring, the second dictionary will override when
# they share keys, which is the desired behavior.
# This allows user to only provide elements they want to override, and for us to still provide any
# defaults they don't want to change
pod_spec = deepmerge(default_pod_spec, pod_spec_override)
pod_spec['spec']['containers'][0]['image'] = ee.image
pod_spec['spec']['containers'][0]['args'] = ['ansible-runner', 'worker', '--private-data-dir=/runner']
# Enforce EE Pull Policy
pull_options = {"always": "Always", "missing": "IfNotPresent", "never": "Never"}
if self.task and self.task.instance.execution_environment:
if self.task.instance.execution_environment.pull:
pod_spec['spec']['containers'][0]['imagePullPolicy'] = pull_options[self.task.instance.execution_environment.pull]
if self.task and self.task.instance.is_container_group_task:
# If EE credential is passed, create an imagePullSecret
if self.task.instance.execution_environment and self.task.instance.execution_environment.credential:
# Create pull secret in k8s cluster based on ee cred
from awx.main.scheduler.kubernetes import PodManager # prevent circular import
pm = PodManager(self.task.instance)
secret_name = pm.create_secret(job=self.task.instance)
# Inject secret name into podspec
pod_spec['spec']['imagePullSecrets'] = [{"name": secret_name}]
if self.task:
pod_spec['metadata'] = deepmerge(
pod_spec.get('metadata', {}),
dict(name=self.pod_name, labels={'ansible-awx': settings.INSTALL_UUID, 'ansible-awx-job-id': str(self.task.instance.id)}),
)
return pod_spec
@property
def pod_name(self):
return f"automation-job-{self.task.instance.id}"
@property
def credential(self):
return self.task.instance.instance_group.credential
@property
def namespace(self):
return self.pod_definition['metadata']['namespace']
@property
def kube_config(self):
host_input = self.credential.get_input('host')
config = {
"apiVersion": "v1",
"kind": "Config",
"preferences": {},
"clusters": [{"name": host_input, "cluster": {"server": host_input}}],
"users": [{"name": host_input, "user": {"token": self.credential.get_input('bearer_token')}}],
"contexts": [{"name": host_input, "context": {"cluster": host_input, "user": host_input, "namespace": self.namespace}}],
"current-context": host_input,
}
if self.credential.get_input('verify_ssl') and 'ssl_ca_cert' in self.credential.inputs:
config["clusters"][0]["cluster"]["certificate-authority-data"] = b64encode(
self.credential.get_input('ssl_ca_cert').encode() # encode to bytes
).decode() # decode the base64 data into a str
else:
config["clusters"][0]["cluster"]["insecure-skip-tls-verify"] = True
return config

897
awx/main/tasks/system.py Normal file
View File

@@ -0,0 +1,897 @@
# Python
from collections import namedtuple
import functools
import importlib
import json
import logging
import os
from io import StringIO
from contextlib import redirect_stdout
import shutil
import time
from distutils.version import LooseVersion as Version
# 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.encoding import smart_str
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_noop
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
# Django-CRUM
from crum import impersonate
# Runner
import ansible_runner.cleanup
# dateutil
from dateutil.parser import parse as parse_date
# AWX
from awx import __version__ as awx_application_version
from awx.main.access import access_registry
from awx.main.models import (
Schedule,
TowerScheduleState,
Instance,
InstanceGroup,
UnifiedJob,
Notification,
Inventory,
SmartInventoryMembership,
Job,
)
from awx.main.constants import ACTIVE_STATES
from awx.main.dispatch.publish import task
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,
)
from awx.main.utils.external_logging import reconfigure_rsyslog
from awx.main.utils.reload import stop_local_services
from awx.main.utils.pglock import advisory_lock
from awx.main.tasks.receptor import get_receptor_ctl, worker_info, worker_cleanup, administrative_workunit_reaper
from awx.main.consumers import emit_channel_notification
from awx.main import analytics
from awx.conf import settings_registry
from awx.main.analytics.subsystem_metrics import Metrics
from rest_framework.exceptions import PermissionDenied
logger = logging.getLogger('awx.main.tasks.system')
OPENSSH_KEY_ERROR = u'''\
It looks like you're trying to use a private key in OpenSSH format, which \
isn't supported by the installed version of OpenSSH on this instance. \
Try upgrading OpenSSH or providing your private key in an different format. \
'''
def dispatch_startup():
startup_logger = logging.getLogger('awx.main.tasks')
startup_logger.debug("Syncing Schedules")
for sch in Schedule.objects.all():
try:
sch.update_computed_fields()
except Exception:
logger.exception("Failed to rebuild schedule {}.".format(sch))
#
# When the dispatcher starts, if the instance cannot be found in the database,
# automatically register it. This is mostly useful for openshift-based
# deployments where:
#
# 2 Instances come online
# Instance B encounters a network blip, Instance A notices, and
# deprovisions it
# Instance B's connectivity is restored, the dispatcher starts, and it
# re-registers itself
#
# In traditional container-less deployments, instances don't get
# deprovisioned when they miss their heartbeat, so this code is mostly a
# no-op.
#
apply_cluster_membership_policies()
cluster_node_heartbeat()
Metrics().clear_values()
# Update Tower's rsyslog.conf file based on loggins settings in the db
reconfigure_rsyslog()
def inform_cluster_of_shutdown():
try:
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)
except Exception:
logger.exception('failed to reap 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.')
@task(queue=get_local_queuename)
def apply_cluster_membership_policies():
from awx.main.signals import disable_activity_stream
started_waiting = time.time()
with advisory_lock('cluster_policy_lock', wait=True):
lock_time = time.time() - started_waiting
if lock_time > 1.0:
to_log = logger.info
else:
to_log = logger.debug
to_log('Waited {} seconds to obtain lock name: cluster_policy_lock'.format(lock_time))
started_compute = time.time()
# Hop nodes should never get assigned to an InstanceGroup.
all_instances = list(Instance.objects.exclude(node_type='hop').order_by('id'))
all_groups = list(InstanceGroup.objects.prefetch_related('instances'))
total_instances = len(all_instances)
actual_groups = []
actual_instances = []
Group = namedtuple('Group', ['obj', 'instances', 'prior_instances'])
Node = namedtuple('Instance', ['obj', 'groups'])
# Process policy instance list first, these will represent manually managed memberships
instance_hostnames_map = {inst.hostname: inst for inst in all_instances}
for ig in all_groups:
group_actual = Group(obj=ig, instances=[], prior_instances=[instance.pk for instance in ig.instances.all()]) # obtained in prefetch
for hostname in ig.policy_instance_list:
if hostname not in instance_hostnames_map:
logger.info("Unknown instance {} in {} policy list".format(hostname, ig.name))
continue
inst = instance_hostnames_map[hostname]
group_actual.instances.append(inst.id)
# NOTE: arguable behavior: policy-list-group is not added to
# instance's group count for consideration in minimum-policy rules
if group_actual.instances:
logger.debug("Policy List, adding Instances {} to Group {}".format(group_actual.instances, ig.name))
actual_groups.append(group_actual)
# Process Instance minimum policies next, since it represents a concrete lower bound to the
# number of instances to make available to instance groups
actual_instances = [Node(obj=i, groups=[]) for i in all_instances if i.managed_by_policy]
logger.debug("Total instances: {}, available for policy: {}".format(total_instances, len(actual_instances)))
for g in sorted(actual_groups, key=lambda x: len(x.instances)):
exclude_type = 'execution' if g.obj.name == settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME else 'control'
policy_min_added = []
for i in sorted(actual_instances, key=lambda x: len(x.groups)):
if i.obj.node_type == exclude_type:
continue # never place execution instances in controlplane group or control instances in other groups
if len(g.instances) >= g.obj.policy_instance_minimum:
break
if i.obj.id in g.instances:
# If the instance is already _in_ the group, it was
# applied earlier via the policy list
continue
g.instances.append(i.obj.id)
i.groups.append(g.obj.id)
policy_min_added.append(i.obj.id)
if policy_min_added:
logger.debug("Policy minimum, adding Instances {} to Group {}".format(policy_min_added, g.obj.name))
# Finally, process instance policy percentages
for g in sorted(actual_groups, key=lambda x: len(x.instances)):
exclude_type = 'execution' if g.obj.name == settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME else 'control'
candidate_pool_ct = sum(1 for i in actual_instances if i.obj.node_type != exclude_type)
if not candidate_pool_ct:
continue
policy_per_added = []
for i in sorted(actual_instances, key=lambda x: len(x.groups)):
if i.obj.node_type == exclude_type:
continue
if i.obj.id in g.instances:
# If the instance is already _in_ the group, it was
# applied earlier via a minimum policy or policy list
continue
if 100 * float(len(g.instances)) / candidate_pool_ct >= g.obj.policy_instance_percentage:
break
g.instances.append(i.obj.id)
i.groups.append(g.obj.id)
policy_per_added.append(i.obj.id)
if policy_per_added:
logger.debug("Policy percentage, adding Instances {} to Group {}".format(policy_per_added, g.obj.name))
# Determine if any changes need to be made
needs_change = False
for g in actual_groups:
if set(g.instances) != set(g.prior_instances):
needs_change = True
break
if not needs_change:
logger.debug('Cluster policy no-op finished in {} seconds'.format(time.time() - started_compute))
return
# On a differential basis, apply instances to groups
with transaction.atomic():
with disable_activity_stream():
for g in actual_groups:
if g.obj.is_container_group:
logger.debug('Skipping containerized group {} for policy calculation'.format(g.obj.name))
continue
instances_to_add = set(g.instances) - set(g.prior_instances)
instances_to_remove = set(g.prior_instances) - set(g.instances)
if instances_to_add:
logger.debug('Adding instances {} to group {}'.format(list(instances_to_add), g.obj.name))
g.obj.instances.add(*instances_to_add)
if instances_to_remove:
logger.debug('Removing instances {} from group {}'.format(list(instances_to_remove), g.obj.name))
g.obj.instances.remove(*instances_to_remove)
logger.debug('Cluster policy computation finished in {} seconds'.format(time.time() - started_compute))
@task(queue='tower_broadcast_all')
def handle_setting_changes(setting_keys):
orig_len = len(setting_keys)
for i in range(orig_len):
for dependent_key in settings_registry.get_dependent_settings(setting_keys[i]):
setting_keys.append(dependent_key)
cache_keys = set(setting_keys)
logger.debug('cache delete_many(%r)', cache_keys)
cache.delete_many(cache_keys)
if any([setting.startswith('LOG_AGGREGATOR') for setting in setting_keys]):
reconfigure_rsyslog()
@task(queue='tower_broadcast_all')
def delete_project_files(project_path):
# TODO: possibly implement some retry logic
lock_file = project_path + '.lock'
if os.path.exists(project_path):
try:
shutil.rmtree(project_path)
logger.debug('Success removing project files {}'.format(project_path))
except Exception:
logger.exception('Could not remove project directory {}'.format(project_path))
if os.path.exists(lock_file):
try:
os.remove(lock_file)
logger.debug('Success removing {}'.format(lock_file))
except Exception:
logger.exception('Could not remove lock file {}'.format(lock_file))
@task(queue='tower_broadcast_all')
def profile_sql(threshold=1, minutes=1):
if threshold <= 0:
cache.delete('awx-profile-sql-threshold')
logger.error('SQL PROFILING DISABLED')
else:
cache.set('awx-profile-sql-threshold', threshold, timeout=minutes * 60)
logger.error('SQL QUERIES >={}s ENABLED FOR {} MINUTE(S)'.format(threshold, minutes))
@task(queue=get_local_queuename)
def send_notifications(notification_list, job_id=None):
if not isinstance(notification_list, list):
raise TypeError("notification_list should be of type list")
if job_id is not None:
job_actual = UnifiedJob.objects.get(id=job_id)
notifications = Notification.objects.filter(id__in=notification_list)
if job_id is not None:
job_actual.notifications.add(*notifications)
for notification in notifications:
update_fields = ['status', 'notifications_sent']
try:
sent = notification.notification_template.send(notification.subject, notification.body)
notification.status = "successful"
notification.notifications_sent = sent
if job_id is not None:
job_actual.log_lifecycle("notifications_sent")
except Exception as e:
logger.exception("Send Notification Failed {}".format(e))
notification.status = "failed"
notification.error = smart_str(e)
update_fields.append('error')
finally:
try:
notification.save(update_fields=update_fields)
except Exception:
logger.exception('Error saving notification {} result.'.format(notification.id))
@task(queue=get_local_queuename)
def gather_analytics():
from awx.conf.models import Setting
from rest_framework.fields import DateTimeField
last_gather = Setting.objects.filter(key='AUTOMATION_ANALYTICS_LAST_GATHER').first()
last_time = DateTimeField().to_internal_value(last_gather.value) if last_gather and last_gather.value else None
gather_time = now()
if not last_time or ((gather_time - last_time).total_seconds() > settings.AUTOMATION_ANALYTICS_GATHER_INTERVAL):
analytics.gather()
@task(queue=get_local_queuename)
def purge_old_stdout_files():
nowtime = time.time()
for f in os.listdir(settings.JOBOUTPUT_ROOT):
if os.path.getctime(os.path.join(settings.JOBOUTPUT_ROOT, f)) < nowtime - settings.LOCAL_STDOUT_EXPIRE_TIME:
os.unlink(os.path.join(settings.JOBOUTPUT_ROOT, f))
logger.debug("Removing {}".format(os.path.join(settings.JOBOUTPUT_ROOT, f)))
def _cleanup_images_and_files(**kwargs):
if settings.IS_K8S:
return
this_inst = Instance.objects.me()
runner_cleanup_kwargs = this_inst.get_cleanup_task_kwargs(**kwargs)
if runner_cleanup_kwargs:
stdout = ''
with StringIO() as buffer:
with redirect_stdout(buffer):
ansible_runner.cleanup.run_cleanup(runner_cleanup_kwargs)
stdout = buffer.getvalue()
if '(changed: True)' in stdout:
logger.info(f'Performed local cleanup with kwargs {kwargs}, output:\n{stdout}')
# if we are the first instance alphabetically, then run cleanup on execution nodes
checker_instance = Instance.objects.filter(node_type__in=['hybrid', 'control'], enabled=True, capacity__gt=0).order_by('-hostname').first()
if checker_instance and this_inst.hostname == checker_instance.hostname:
for inst in Instance.objects.filter(node_type='execution', enabled=True, capacity__gt=0):
runner_cleanup_kwargs = inst.get_cleanup_task_kwargs(**kwargs)
if not runner_cleanup_kwargs:
continue
try:
stdout = worker_cleanup(inst.hostname, runner_cleanup_kwargs)
if '(changed: True)' in stdout:
logger.info(f'Performed cleanup on execution node {inst.hostname} with output:\n{stdout}')
except RuntimeError:
logger.exception(f'Error running cleanup on execution node {inst.hostname}')
@task(queue='tower_broadcast_all')
def handle_removed_image(remove_images=None):
"""Special broadcast invocation of this method to handle case of deleted EE"""
_cleanup_images_and_files(remove_images=remove_images, file_pattern='')
@task(queue=get_local_queuename)
def cleanup_images_and_files():
_cleanup_images_and_files()
@task(queue=get_local_queuename)
def cluster_node_health_check(node):
"""
Used for the health check endpoint, refreshes the status of the instance, but must be ran on target node
"""
if node == '':
logger.warn('Local health check incorrectly called with blank string')
return
elif node != settings.CLUSTER_HOST_ID:
logger.warn(f'Local health check for {node} incorrectly sent to {settings.CLUSTER_HOST_ID}')
return
try:
this_inst = Instance.objects.me()
except Instance.DoesNotExist:
logger.warn(f'Instance record for {node} missing, could not check capacity.')
return
this_inst.local_health_check()
@task(queue=get_local_queuename)
def execution_node_health_check(node):
if node == '':
logger.warn('Remote health check incorrectly called with blank string')
return
try:
instance = Instance.objects.get(hostname=node)
except Instance.DoesNotExist:
logger.warn(f'Instance record for {node} missing, could not check capacity.')
return
if instance.node_type != 'execution':
raise RuntimeError(f'Execution node health check ran against {instance.node_type} node {instance.hostname}')
data = worker_info(node)
prior_capacity = instance.capacity
instance.save_health_data(
version='ansible-runner-' + data.get('runner_version', '???'),
cpu=data.get('cpu_count', 0),
memory=data.get('mem_in_bytes', 0),
uuid=data.get('uuid'),
errors='\n'.join(data.get('errors', [])),
)
if data['errors']:
formatted_error = "\n".join(data["errors"])
if prior_capacity:
logger.warn(f'Health check marking execution node {node} as lost, errors:\n{formatted_error}')
else:
logger.info(f'Failed to find capacity of new or lost execution node {node}, errors:\n{formatted_error}')
else:
logger.info('Set capacity of execution node {} to {}, worker info data:\n{}'.format(node, instance.capacity, json.dumps(data, indent=2)))
return data
def inspect_execution_nodes(instance_list):
with advisory_lock('inspect_execution_nodes_lock', wait=False):
node_lookup = {inst.hostname: inst for inst in instance_list}
ctl = get_receptor_ctl()
mesh_status = ctl.simple_command('status')
nowtime = now()
workers = mesh_status['Advertisements']
for ad in workers:
hostname = ad['NodeID']
if not any(cmd['WorkType'] == 'ansible-runner' for cmd in ad['WorkCommands'] or []):
continue
changed = False
if hostname in node_lookup:
instance = node_lookup[hostname]
else:
logger.warn(f"Unrecognized node on mesh advertising ansible-runner work type: {hostname}")
continue
was_lost = instance.is_lost(ref_time=nowtime)
last_seen = parse_date(ad['Time'])
if instance.last_seen and instance.last_seen >= last_seen:
continue
instance.last_seen = last_seen
instance.save(update_fields=['last_seen'])
if changed:
execution_node_health_check.apply_async([hostname])
elif was_lost:
# if the instance *was* lost, but has appeared again,
# attempt to re-establish the initial capacity and version
# check
logger.warn(f'Execution node attempting to rejoin as instance {hostname}.')
execution_node_health_check.apply_async([hostname])
elif instance.capacity == 0 and instance.enabled:
# nodes with proven connection but need remediation run health checks are reduced frequency
if not instance.last_health_check or (nowtime - instance.last_health_check).total_seconds() >= settings.EXECUTION_NODE_REMEDIATION_CHECKS:
# Periodically re-run the health check of errored nodes, in case someone fixed it
# TODO: perhaps decrease the frequency of these checks
logger.debug(f'Restarting health check for execution node {hostname} with known errors.')
execution_node_health_check.apply_async([hostname])
@task(queue=get_local_queuename)
def cluster_node_heartbeat():
logger.debug("Cluster node heartbeat task.")
nowtime = now()
instance_list = list(Instance.objects.all())
this_inst = None
lost_instances = []
for inst in instance_list:
if inst.hostname == settings.CLUSTER_HOST_ID:
this_inst = inst
instance_list.remove(inst)
break
else:
(changed, this_inst) = Instance.objects.get_or_register()
if changed:
logger.info("Registered tower control node '{}'".format(this_inst.hostname))
inspect_execution_nodes(instance_list)
for inst in list(instance_list):
if inst.is_lost(ref_time=nowtime):
lost_instances.append(inst)
instance_list.remove(inst)
if this_inst:
startup_event = this_inst.is_lost(ref_time=nowtime)
this_inst.local_health_check()
if startup_event and this_inst.capacity != 0:
logger.warning('Rejoining the cluster as instance {}.'.format(this_inst.hostname))
return
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.version == "" or other_inst.version.startswith('ansible-runner') or other_inst.node_type == 'execution':
continue
if Version(other_inst.version.split('-', 1)[0]) > Version(awx_application_version.split('-', 1)[0]) and not settings.DEBUG:
logger.error(
"Host {} reports version {}, but this node {} is at {}, shutting down".format(
other_inst.hostname, other_inst.version, this_inst.hostname, this_inst.version
)
)
# Shutdown signal will set the capacity to zero to ensure no Jobs get added to this instance.
# The heartbeat task will reset the capacity to the system capacity after upgrade.
stop_local_services(communicate=False)
raise RuntimeError("Shutting down.")
for other_inst in lost_instances:
try:
reaper.reap(other_inst)
except Exception:
logger.exception('failed to reap jobs for {}'.format(other_inst.hostname))
try:
# Capacity could already be 0 because:
# * It's a new node and it never had a heartbeat
# * It was set to 0 by another tower node running this method
# * It was set to 0 by this node, but auto deprovisioning is off
#
# If auto deprovisining is on, don't bother setting the capacity to 0
# since we will delete the node anyway.
if other_inst.capacity != 0 and not settings.AWX_AUTO_DEPROVISION_INSTANCES:
other_inst.mark_offline(errors=_('Another cluster node has determined this instance to be unresponsive'))
logger.error("Host {} last checked in at {}, marked as lost.".format(other_inst.hostname, other_inst.last_seen))
elif settings.AWX_AUTO_DEPROVISION_INSTANCES:
deprovision_hostname = other_inst.hostname
other_inst.delete()
logger.info("Host {} Automatically Deprovisioned.".format(deprovision_hostname))
except DatabaseError as e:
if 'did not affect any rows' in str(e):
logger.debug('Another instance has marked {} as lost'.format(other_inst.hostname))
else:
logger.exception('Error marking {} as lost'.format(other_inst.hostname))
@task(queue=get_local_queuename)
def awx_receptor_workunit_reaper():
"""
When an AWX job is launched via receptor, files such as status, stdin, and stdout are created
in a specific receptor directory. This directory on disk is a random 8 character string, e.g. qLL2JFNT
This is also called the work Unit ID in receptor, and is used in various receptor commands,
e.g. "work results qLL2JFNT"
After an AWX job executes, the receptor work unit directory is cleaned up by
issuing the work release command. In some cases the release process might fail, or
if AWX crashes during a job's execution, the work release command is never issued to begin with.
As such, this periodic task will obtain a list of all receptor work units, and find which ones
belong to AWX jobs that are in a completed state (status is canceled, error, or succeeded).
This task will call "work release" on each of these work units to clean up the files on disk.
Note that when we call "work release" on a work unit that actually represents remote work
both the local and remote work units are cleaned up.
Since we are cleaning up jobs that controller considers to be inactive, we take the added
precaution of calling "work cancel" in case the work unit is still active.
"""
if not settings.RECEPTOR_RELEASE_WORK:
return
logger.debug("Checking for unreleased receptor work units")
receptor_ctl = get_receptor_ctl()
receptor_work_list = receptor_ctl.simple_command("work list")
unit_ids = [id for id in receptor_work_list]
jobs_with_unreleased_receptor_units = UnifiedJob.objects.filter(work_unit_id__in=unit_ids).exclude(status__in=ACTIVE_STATES)
for job in jobs_with_unreleased_receptor_units:
logger.debug(f"{job.log_format} is not active, reaping receptor work unit {job.work_unit_id}")
receptor_ctl.simple_command(f"work cancel {job.work_unit_id}")
receptor_ctl.simple_command(f"work release {job.work_unit_id}")
administrative_workunit_reaper(receptor_work_list)
@task(queue=get_local_queuename)
def awx_k8s_reaper():
if not settings.RECEPTOR_RELEASE_WORK:
return
from awx.main.scheduler.kubernetes import PodManager # prevent circular import
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):
logger.debug('{} is no longer active, reaping orphaned k8s pod'.format(job.log_format))
try:
pm = PodManager(job)
pm.kube_api.delete_namespaced_pod(name=pods[job.id], namespace=pm.namespace, _request_timeout=settings.AWX_CONTAINER_GROUP_K8S_API_TIMEOUT)
except Exception:
logger.exception("Failed to delete orphaned pod {} from {}".format(job.log_format, group))
@task(queue=get_local_queuename)
def awx_periodic_scheduler():
with advisory_lock('awx_periodic_scheduler_lock', wait=False) as acquired:
if acquired is False:
logger.debug("Not running periodic scheduler, another task holds lock")
return
logger.debug("Starting periodic scheduler")
run_now = now()
state = TowerScheduleState.get_solo()
last_run = state.schedule_last_run
logger.debug("Last scheduler run was: %s", last_run)
state.schedule_last_run = run_now
state.save()
old_schedules = Schedule.objects.enabled().before(last_run)
for schedule in old_schedules:
schedule.update_computed_fields()
schedules = Schedule.objects.enabled().between(last_run, run_now)
invalid_license = False
try:
access_registry[Job](None).check_license(quiet=True)
except PermissionDenied as e:
invalid_license = e
for schedule in schedules:
template = schedule.unified_job_template
schedule.update_computed_fields() # To update next_run timestamp.
if template.cache_timeout_blocked:
logger.warn("Cache timeout is in the future, bypassing schedule for template %s" % str(template.id))
continue
try:
job_kwargs = schedule.get_job_kwargs()
new_unified_job = schedule.unified_job_template.create_unified_job(**job_kwargs)
logger.debug('Spawned {} from schedule {}-{}.'.format(new_unified_job.log_format, schedule.name, schedule.pk))
if invalid_license:
new_unified_job.status = 'failed'
new_unified_job.job_explanation = str(invalid_license)
new_unified_job.save(update_fields=['status', 'job_explanation'])
new_unified_job.websocket_emit_status("failed")
raise invalid_license
can_start = new_unified_job.signal_start()
except Exception:
logger.exception('Error spawning scheduled job.')
continue
if not can_start:
new_unified_job.status = 'failed'
new_unified_job.job_explanation = gettext_noop(
"Scheduled job could not start because it \
was not in the right state or required manual credentials"
)
new_unified_job.save(update_fields=['status', 'job_explanation'])
new_unified_job.websocket_emit_status("failed")
emit_channel_notification('schedules-changed', dict(id=schedule.id, group_name="schedules"))
state.save()
@task(queue=get_local_queuename)
def handle_work_success(task_actual):
try:
instance = UnifiedJob.get_instance_by_type(task_actual['type'], task_actual['id'])
except ObjectDoesNotExist:
logger.warning('Missing {} `{}` in success callback.'.format(task_actual['type'], task_actual['id']))
return
if not instance:
return
schedule_task_manager()
@task(queue=get_local_queuename)
def handle_work_error(task_id, *args, **kwargs):
subtasks = kwargs.get('subtasks', None)
logger.debug('Executing error task id %s, subtasks: %s' % (task_id, str(subtasks)))
first_instance = None
first_instance_type = ''
if subtasks is not None:
for each_task in subtasks:
try:
instance = UnifiedJob.get_instance_by_type(each_task['type'], each_task['id'])
if not instance:
# Unknown task type
logger.warn("Unknown task type: {}".format(each_task['type']))
continue
except ObjectDoesNotExist:
logger.warning('Missing {} `{}` in error callback.'.format(each_task['type'], each_task['id']))
continue
if first_instance is None:
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':
instance.status = 'failed'
instance.failed = True
if not instance.job_explanation:
instance.job_explanation = 'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % (
first_instance_type,
first_instance.name,
first_instance.id,
)
instance.save()
instance.websocket_emit_status("failed")
# We only send 1 job complete message since all the job completion message
# handling does is trigger the scheduler. If we extend the functionality of
# 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 < 5:
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.warn(f"Failed to even try to send notifications for job '{uj}' due to job not being in finished state.")
@task(queue=get_local_queuename)
def update_inventory_computed_fields(inventory_id):
"""
Signal handler and wrapper around inventory.update_computed_fields to
prevent unnecessary recursive calls.
"""
i = Inventory.objects.filter(id=inventory_id)
if not i.exists():
logger.error("Update Inventory Computed Fields failed due to missing inventory: " + str(inventory_id))
return
i = i[0]
try:
i.update_computed_fields()
except DatabaseError as e:
if 'did not affect any rows' in str(e):
logger.debug('Exiting duplicate update_inventory_computed_fields task.')
return
raise
def update_smart_memberships_for_inventory(smart_inventory):
current = set(SmartInventoryMembership.objects.filter(inventory=smart_inventory).values_list('host_id', flat=True))
new = set(smart_inventory.hosts.values_list('id', flat=True))
additions = new - current
removals = current - new
if additions or removals:
with transaction.atomic():
if removals:
SmartInventoryMembership.objects.filter(inventory=smart_inventory, host_id__in=removals).delete()
if additions:
add_for_inventory = [SmartInventoryMembership(inventory_id=smart_inventory.id, host_id=host_id) for host_id in additions]
SmartInventoryMembership.objects.bulk_create(add_for_inventory, ignore_conflicts=True)
logger.debug(
'Smart host membership cached for {}, {} additions, {} removals, {} total count.'.format(
smart_inventory.pk, len(additions), len(removals), len(new)
)
)
return True # changed
return False
@task(queue=get_local_queuename)
def update_host_smart_inventory_memberships():
smart_inventories = Inventory.objects.filter(kind='smart', host_filter__isnull=False, pending_deletion=False)
changed_inventories = set([])
for smart_inventory in smart_inventories:
try:
changed = update_smart_memberships_for_inventory(smart_inventory)
if changed:
changed_inventories.add(smart_inventory)
except IntegrityError:
logger.exception('Failed to update smart inventory memberships for {}'.format(smart_inventory.pk))
# Update computed fields for changed inventories outside atomic action
for smart_inventory in changed_inventories:
smart_inventory.update_computed_fields()
@task(queue=get_local_queuename)
def delete_inventory(inventory_id, user_id, retries=5):
# Delete inventory as user
if user_id is None:
user = None
else:
try:
user = User.objects.get(id=user_id)
except Exception:
user = None
with ignore_inventory_computed_fields(), ignore_inventory_group_removal(), impersonate(user):
try:
i = Inventory.objects.get(id=inventory_id)
for host in i.hosts.iterator():
host.job_events_as_primary_host.update(host=None)
i.delete()
emit_channel_notification('inventories-status_changed', {'group_name': 'inventories', 'inventory_id': inventory_id, 'status': 'deleted'})
logger.debug('Deleted inventory {} as user {}.'.format(inventory_id, user_id))
except Inventory.DoesNotExist:
logger.exception("Delete Inventory failed due to missing inventory: " + str(inventory_id))
return
except DatabaseError:
logger.exception('Database error deleting inventory {}, but will retry.'.format(inventory_id))
if retries > 0:
time.sleep(10)
delete_inventory(inventory_id, user_id, retries=retries - 1)
def with_path_cleanup(f):
@functools.wraps(f)
def _wrapped(self, *args, **kwargs):
try:
return f(self, *args, **kwargs)
finally:
for p in self.cleanup_paths:
try:
if os.path.isdir(p):
shutil.rmtree(p, ignore_errors=True)
elif os.path.exists(p):
os.remove(p)
except OSError:
logger.exception("Failed to remove tmp file: {}".format(p))
self.cleanup_paths = []
return _wrapped
def _reconstruct_relationships(copy_mapping):
for old_obj, new_obj in copy_mapping.items():
model = type(old_obj)
for field_name in getattr(model, 'FIELDS_TO_PRESERVE_AT_COPY', []):
field = model._meta.get_field(field_name)
if isinstance(field, ForeignKey):
if getattr(new_obj, field_name, None):
continue
related_obj = getattr(old_obj, field_name)
related_obj = copy_mapping.get(related_obj, related_obj)
setattr(new_obj, field_name, related_obj)
elif field.many_to_many:
for related_obj in getattr(old_obj, field_name).all():
logger.debug('Deep copy: Adding {} to {}({}).{} relationship'.format(related_obj, new_obj, model, field_name))
getattr(new_obj, field_name).add(copy_mapping.get(related_obj, related_obj))
new_obj.save()
@task(queue=get_local_queuename)
def deep_copy_model_obj(model_module, model_name, obj_pk, new_obj_pk, user_pk, uuid, permission_check_func=None):
sub_obj_list = cache.get(uuid)
if sub_obj_list is None:
logger.error('Deep copy {} from {} to {} failed unexpectedly.'.format(model_name, obj_pk, new_obj_pk))
return
logger.debug('Deep copy {} from {} to {}.'.format(model_name, obj_pk, new_obj_pk))
from awx.api.generics import CopyAPIView
from awx.main.signals import disable_activity_stream
model = getattr(importlib.import_module(model_module), model_name, None)
if model is None:
return
try:
obj = model.objects.get(pk=obj_pk)
new_obj = model.objects.get(pk=new_obj_pk)
creater = User.objects.get(pk=user_pk)
except ObjectDoesNotExist:
logger.warning("Object or user no longer exists.")
return
with transaction.atomic(), ignore_inventory_computed_fields(), disable_activity_stream():
copy_mapping = {}
for sub_obj_setup in sub_obj_list:
sub_model = getattr(importlib.import_module(sub_obj_setup[0]), sub_obj_setup[1], None)
if sub_model is None:
continue
try:
sub_obj = sub_model.objects.get(pk=sub_obj_setup[2])
except ObjectDoesNotExist:
continue
copy_mapping.update(CopyAPIView.copy_model_obj(obj, new_obj, sub_model, sub_obj, creater))
_reconstruct_relationships(copy_mapping)
if permission_check_func:
permission_check_func = getattr(getattr(importlib.import_module(permission_check_func[0]), permission_check_func[1]), permission_check_func[2])
permission_check_func(creater, copy_mapping.values())
if isinstance(new_obj, Inventory):
update_inventory_computed_fields.delay(new_obj.id)

View File

@@ -84,6 +84,11 @@ def default_instance_group(instance_factory, instance_group_factory):
return create_instance_group("default", instances=[create_instance("hostA")])
@pytest.fixture
def controlplane_instance_group(instance_factory, instance_group_factory):
return create_instance_group("controlplane", instances=[create_instance("hostA")])
@pytest.fixture
def job_template_with_survey_passwords_factory(job_template_factory):
def rf(persisted):

View File

@@ -0,0 +1,82 @@
import pytest
from unittest import mock
from awx.api.versioning import reverse
from awx.main.models.activity_stream import ActivityStream
from awx.main.models.ha import Instance
import redis
# Django
from django.test.utils import override_settings
INSTANCE_KWARGS = dict(hostname='example-host', cpu=6, memory=36000000000, cpu_capacity=6, mem_capacity=42)
@pytest.mark.django_db
def test_disabled_zeros_capacity(patch, admin_user):
instance = Instance.objects.create(**INSTANCE_KWARGS)
assert ActivityStream.objects.filter(instance=instance).count() == 1
url = reverse('api:instance_detail', kwargs={'pk': instance.pk})
r = patch(url=url, data={'enabled': False}, user=admin_user)
assert r.data['capacity'] == 0
instance.refresh_from_db()
assert instance.capacity == 0
assert ActivityStream.objects.filter(instance=instance).count() == 2
@pytest.mark.django_db
def test_enabled_sets_capacity(patch, admin_user):
instance = Instance.objects.create(enabled=False, capacity=0, **INSTANCE_KWARGS)
assert instance.capacity == 0
assert ActivityStream.objects.filter(instance=instance).count() == 1
url = reverse('api:instance_detail', kwargs={'pk': instance.pk})
r = patch(url=url, data={'enabled': True}, user=admin_user)
assert r.data['capacity'] > 0
instance.refresh_from_db()
assert instance.capacity > 0
assert ActivityStream.objects.filter(instance=instance).count() == 2
@pytest.mark.django_db
def test_auditor_user_health_check(get, post, system_auditor):
instance = Instance.objects.create(**INSTANCE_KWARGS)
url = reverse('api:instance_health_check', kwargs={'pk': instance.pk})
r = get(url=url, user=system_auditor, expect=200)
assert r.data['cpu_capacity'] == instance.cpu_capacity
post(url=url, user=system_auditor, expect=403)
@pytest.mark.django_db
def test_health_check_throws_error(post, admin_user):
instance = Instance.objects.create(node_type='execution', **INSTANCE_KWARGS)
url = reverse('api:instance_health_check', kwargs={'pk': instance.pk})
# we will simulate a receptor error, similar to this one
# https://github.com/ansible/receptor/blob/156e6e24a49fbf868734507f9943ac96208ed8f5/receptorctl/receptorctl/socket_interface.py#L204
# related to issue https://github.com/ansible/tower/issues/5315
with mock.patch('awx.main.tasks.receptor.run_until_complete', side_effect=RuntimeError('Remote error: foobar')):
post(url=url, user=admin_user, expect=200)
instance.refresh_from_db()
assert 'Remote error: foobar' in instance.errors
assert instance.capacity == 0
@pytest.mark.django_db
@mock.patch.object(redis.client.Redis, 'ping', lambda self: True)
def test_health_check_usage(get, post, admin_user):
instance = Instance.objects.create(**INSTANCE_KWARGS)
url = reverse('api:instance_health_check', kwargs={'pk': instance.pk})
r = get(url=url, user=admin_user, expect=200)
assert r.data['cpu_capacity'] == instance.cpu_capacity
assert r.data['last_health_check'] is None
with override_settings(CLUSTER_HOST_ID=instance.hostname): # force direct call of cluster_node_health_check
r = post(url=url, user=admin_user, expect=200)
assert r.data['last_health_check'] is not None

View File

@@ -4,6 +4,7 @@ import pytest
from awx.api.versioning import reverse
from awx.main.models import (
ActivityStream,
Instance,
InstanceGroup,
ProjectUpdate,
@@ -23,6 +24,14 @@ def instance():
return Instance.objects.create(hostname='instance')
@pytest.fixture
def node_type_instance():
def fn(hostname, node_type):
return Instance.objects.create(hostname=hostname, node_type=node_type)
return fn
@pytest.fixture
def instance_group(job_factory):
ig = InstanceGroup(name="east")
@@ -198,3 +207,97 @@ def test_containerized_group_default_fields(instance_group, kube_credential):
assert ig.policy_instance_list == []
assert ig.policy_instance_minimum == 0
assert ig.policy_instance_percentage == 0
@pytest.mark.django_db
@pytest.mark.parametrize('node_type', ['control', 'hybrid', 'execution'])
def test_instance_attach_to_instance_group(post, instance_group, node_type_instance, admin, node_type):
instance = node_type_instance(hostname=node_type, node_type=node_type)
count = ActivityStream.objects.count()
url = reverse(f'api:instance_group_instance_list', kwargs={'pk': instance_group.pk})
post(url, {'associate': True, 'id': instance.id}, admin, expect=204 if node_type != 'control' else 400)
new_activity = ActivityStream.objects.all()[count:]
if node_type != 'control':
assert len(new_activity) == 2 # the second is an update of the instance group policy
new_activity = new_activity[0]
assert new_activity.operation == 'associate'
assert new_activity.object1 == 'instance_group'
assert new_activity.object2 == 'instance'
assert new_activity.instance.first() == instance
assert new_activity.instance_group.first() == instance_group
else:
assert not new_activity
@pytest.mark.django_db
@pytest.mark.parametrize('node_type', ['control', 'hybrid', 'execution'])
def test_instance_unattach_from_instance_group(post, instance_group, node_type_instance, admin, node_type):
instance = node_type_instance(hostname=node_type, node_type=node_type)
instance_group.instances.add(instance)
count = ActivityStream.objects.count()
url = reverse(f'api:instance_group_instance_list', kwargs={'pk': instance_group.pk})
post(url, {'disassociate': True, 'id': instance.id}, admin, expect=204 if node_type != 'control' else 400)
new_activity = ActivityStream.objects.all()[count:]
if node_type != 'control':
assert len(new_activity) == 1
new_activity = new_activity[0]
assert new_activity.operation == 'disassociate'
assert new_activity.object1 == 'instance_group'
assert new_activity.object2 == 'instance'
assert new_activity.instance.first() == instance
assert new_activity.instance_group.first() == instance_group
else:
assert not new_activity
@pytest.mark.django_db
@pytest.mark.parametrize('node_type', ['control', 'hybrid', 'execution'])
def test_instance_group_attach_to_instance(post, instance_group, node_type_instance, admin, node_type):
instance = node_type_instance(hostname=node_type, node_type=node_type)
count = ActivityStream.objects.count()
url = reverse(f'api:instance_instance_groups_list', kwargs={'pk': instance.pk})
post(url, {'associate': True, 'id': instance_group.id}, admin, expect=204 if node_type != 'control' else 400)
new_activity = ActivityStream.objects.all()[count:]
if node_type != 'control':
assert len(new_activity) == 2 # the second is an update of the instance group policy
new_activity = new_activity[0]
assert new_activity.operation == 'associate'
assert new_activity.object1 == 'instance'
assert new_activity.object2 == 'instance_group'
assert new_activity.instance.first() == instance
assert new_activity.instance_group.first() == instance_group
else:
assert not new_activity
@pytest.mark.django_db
@pytest.mark.parametrize('node_type', ['control', 'hybrid', 'execution'])
def test_instance_group_unattach_from_instance(post, instance_group, node_type_instance, admin, node_type):
instance = node_type_instance(hostname=node_type, node_type=node_type)
instance_group.instances.add(instance)
count = ActivityStream.objects.count()
url = reverse(f'api:instance_instance_groups_list', kwargs={'pk': instance.pk})
post(url, {'disassociate': True, 'id': instance_group.id}, admin, expect=204 if node_type != 'control' else 400)
new_activity = ActivityStream.objects.all()[count:]
if node_type != 'control':
assert len(new_activity) == 1
new_activity = new_activity[0]
assert new_activity.operation == 'disassociate'
assert new_activity.object1 == 'instance'
assert new_activity.object2 == 'instance_group'
assert new_activity.instance.first() == instance
assert new_activity.instance_group.first() == instance_group
else:
assert not new_activity

View File

@@ -5,6 +5,7 @@
import pytest
from unittest import mock
import os
import yaml
# Django
from django.core.management.base import CommandError
@@ -52,6 +53,110 @@ def mock_logging(self, level):
pass
@pytest.mark.django_db
@mock.patch.object(inventory_import.Command, 'set_logging_level', mock_logging)
class TestMigrationCases:
"""In the case that we have any bugs with the declared instance ID variables
then it is inevitable that we will, at some point, import a host with a blank ID
and then later import it with the correct id.
"""
@pytest.mark.parametrize('id_var', ('', 'foo.id', 'foo.id,other', 'other,foo.id'), ids=['none', 'simple', 'complex', 'backward'])
@pytest.mark.parametrize('host_name', ('host-1', 'fooval'), ids=['arbitrary', 'id'])
@pytest.mark.parametrize('has_var', (True, False))
def test_single_host_not_recreated(self, inventory, id_var, host_name, has_var):
inv_src = InventorySource.objects.create(inventory=inventory, source='gce')
options = dict(overwrite=True, instance_id_var=id_var)
vars = {'foo': {'id': 'fooval'}}
data = {
'_meta': {'hostvars': {host_name: vars if has_var else {'unrelated': 'value'}}},
"ungrouped": {"hosts": [host_name]},
}
old_id = None
for i in range(3):
inventory_import.Command().perform_update(options.copy(), data.copy(), inv_src.create_unified_job())
assert inventory.hosts.count() == inv_src.hosts.count() == 1
host = inventory.hosts.first()
assert host.name == host_name
assert host.instance_id in ('fooval', '')
if has_var:
assert yaml.safe_load(host.variables) == vars
else:
assert yaml.safe_load(host.variables) == {'unrelated': 'value'}
if old_id is not None:
assert host.id == old_id
old_id = host.id
@pytest.mark.parametrize('id_var_seq', [('', 'foo.id,other'), ('foo.id,other', '')], ids=['gained', 'lost']) # second is problem case
@pytest.mark.parametrize('host_name', ('host-1', 'fooval'), ids=['arbitrary', 'id'])
def test_host_gains_or_loses_instance_id(self, inventory, id_var_seq, host_name):
inv_src = InventorySource.objects.create(inventory=inventory, source='gce')
options = dict(overwrite=True)
vars = {'foo': {'id': 'fooval'}}
old_id = None
for id_var in id_var_seq:
options['instance_id_var'] = id_var
data = {
'_meta': {'hostvars': {host_name: vars}},
"ungrouped": {"hosts": [host_name]},
}
inventory_import.Command().perform_update(options.copy(), data.copy(), inv_src.create_unified_job())
assert inventory.hosts.count() == inv_src.hosts.count() == 1
host = inventory.hosts.first()
assert host.name == host_name
assert host.instance_id == ('fooval' if id_var else '')
assert yaml.safe_load(host.variables) == vars
if old_id is not None:
assert host.id == old_id
old_id = host.id
@pytest.mark.parametrize('second_list', [('host-1', 'fooval'), ('host-1',), ('fooval',)])
def test_name_and_id_confusion(self, inventory, second_list):
inv_src = InventorySource.objects.create(inventory=inventory, source='gce')
CASES = [('', ['host-1', 'fooval']), ('foo.id', second_list)]
options = dict(overwrite=True)
vars = {'foo': {'id': 'fooval'}}
data = {
'_meta': {'hostvars': {}},
"ungrouped": {"hosts": []},
}
id_set = None
for id_var, hosts in CASES:
options['instance_id_var'] = id_var
data['_meta']['hostvars'] = {}
for host_name in hosts:
data['_meta']['hostvars'][host_name] = vars if id_var else {}
data['ungrouped']['hosts'] = hosts
inventory_import.Command().perform_update(options.copy(), data.copy(), inv_src.create_unified_job())
new_ids = set(inventory.hosts.values_list('id', flat=True))
if id_set is not None:
assert not (new_ids - id_set)
id_set = new_ids
assert inventory.hosts.count() == len(hosts), [(host.name, host.instance_id) for host in inventory.hosts.all()]
assert inv_src.hosts.count() == len(hosts), [(host.name, host.instance_id) for host in inventory.hosts.all()]
for host_name in hosts:
host = inventory.hosts.get(name=host_name)
assert host.instance_id == ('fooval' if id_var else '')
@pytest.mark.django_db
@pytest.mark.inventory_import
@mock.patch.object(inventory_import.Command, 'check_license', mock.MagicMock())
@@ -89,7 +194,7 @@ class TestINIImports:
def test_inventory_single_ini_import(self, inventory, capsys):
inventory_import.AnsibleInventoryLoader._data = TEST_INVENTORY_CONTENT
cmd = inventory_import.Command()
r = cmd.handle(inventory_id=inventory.pk, source=__file__, method='backport')
r = cmd.handle(inventory_id=inventory.pk, source=__file__)
out, err = capsys.readouterr()
assert r is None
assert out == ''

View File

@@ -0,0 +1,26 @@
from io import StringIO
from contextlib import redirect_stdout
import pytest
from awx.main.management.commands.register_queue import RegisterQueue
from awx.main.models.ha import InstanceGroup
@pytest.mark.django_db
def test_openshift_idempotence():
def perform_register():
with StringIO() as buffer:
with redirect_stdout(buffer):
RegisterQueue('default', 100, 0, [], is_container_group=True).register()
return buffer.getvalue()
assert '(changed: True)' in perform_register()
assert '(changed: True)' not in perform_register()
assert '(changed: True)' not in perform_register()
ig = InstanceGroup.objects.get(name='default')
assert ig.policy_instance_percentage == 100
assert ig.policy_instance_minimum == 0
assert ig.policy_instance_list == []
assert ig.is_container_group is True

View File

@@ -121,7 +121,7 @@ def run_computed_fields_right_away(request):
@pytest.fixture
@mock.patch.object(Project, "update", lambda self, **kwargs: None)
def project(instance, organization):
def project(organization):
prj = Project.objects.create(
name="test-proj",
description="test-proj-desc",
@@ -136,7 +136,7 @@ def project(instance, organization):
@pytest.fixture
@mock.patch.object(Project, "update", lambda self, **kwargs: None)
def manual_project(instance, organization):
def manual_project(organization):
prj = Project.objects.create(
name="test-manual-proj",
description="manual-proj-desc",
@@ -196,7 +196,7 @@ def instance(settings):
@pytest.fixture
def organization(instance):
def organization():
return Organization.objects.create(name="test-org", description="test-org-desc")

View File

@@ -170,7 +170,7 @@ def test_activity_stream_actor(admin_user):
@pytest.mark.django_db
def test_annon_user_action():
def test_anon_user_action():
with mock.patch('awx.main.signals.get_current_user') as u_mock:
u_mock.return_value = AnonymousUser()
inv = Inventory.objects.create(name='ainventory')

View File

@@ -0,0 +1,46 @@
import pytest
from awx.main.models.execution_environments import ExecutionEnvironment
@pytest.fixture
def cleanup_patch(mocker):
return mocker.patch('awx.main.signals.handle_removed_image')
@pytest.mark.django_db
def test_image_unchanged_no_delete_task(cleanup_patch):
"""When an irrelevant EE field is changed, we do not run the image cleanup task"""
execution_environment = ExecutionEnvironment.objects.create(name='test-ee', image='quay.io/foo/bar')
execution_environment.description = 'foobar'
execution_environment.save()
cleanup_patch.delay.assert_not_called()
@pytest.mark.django_db
def test_image_changed_creates_delete_task(cleanup_patch):
execution_environment = ExecutionEnvironment.objects.create(name='test-ee', image='quay.io/foo/bar')
execution_environment.image = 'quay.io/new/image'
execution_environment.save()
cleanup_patch.delay.assert_called_once_with(remove_images=['quay.io/foo/bar'])
@pytest.mark.django_db
def test_image_still_in_use(cleanup_patch):
"""When an image is still in use by another EE, we do not clean it up"""
ExecutionEnvironment.objects.create(name='unrelated-ee', image='quay.io/foo/bar')
execution_environment = ExecutionEnvironment.objects.create(name='test-ee', image='quay.io/foo/bar')
execution_environment.image = 'quay.io/new/image'
execution_environment.save()
cleanup_patch.delay.assert_not_called()
@pytest.mark.django_db
def test_image_deletion_creates_delete_task(cleanup_patch):
execution_environment = ExecutionEnvironment.objects.create(name='test-ee', image='quay.io/foo/bar')
execution_environment.delete()
cleanup_patch.delay.assert_called_once_with(remove_images=['quay.io/foo/bar'])

View File

@@ -5,7 +5,7 @@ from collections import namedtuple
from unittest import mock # noqa
import pytest
from awx.main.tasks import AWXReceptorJob
from awx.main.tasks.receptor import AWXReceptorJob
from awx.main.utils import (
create_temporary_fifo,
)

View File

@@ -3,7 +3,7 @@ from unittest import mock
from datetime import timedelta
from awx.main.scheduler import TaskManager
from awx.main.models import InstanceGroup, WorkflowJob
from awx.main.tasks import apply_cluster_membership_policies
from awx.main.tasks.system import apply_cluster_membership_policies
@pytest.mark.django_db
@@ -30,7 +30,7 @@ def test_multi_group_basic_job_launch(instance_factory, default_instance_group,
@pytest.mark.django_db
def test_multi_group_with_shared_dependency(instance_factory, default_instance_group, mocker, instance_group_factory, job_template_factory):
def test_multi_group_with_shared_dependency(instance_factory, controlplane_instance_group, mocker, instance_group_factory, job_template_factory):
i1 = instance_factory("i1")
i2 = instance_factory("i2")
ig1 = instance_group_factory("ig1", instances=[i1])
@@ -54,7 +54,7 @@ def test_multi_group_with_shared_dependency(instance_factory, default_instance_g
with mocker.patch("awx.main.scheduler.TaskManager.start_task"):
TaskManager().schedule()
pu = p.project_updates.first()
TaskManager.start_task.assert_called_once_with(pu, default_instance_group, [j1, j2], default_instance_group.instances.all()[0])
TaskManager.start_task.assert_called_once_with(pu, controlplane_instance_group, [j1, j2], controlplane_instance_group.instances.all()[0])
pu.finished = pu.created + timedelta(seconds=1)
pu.status = "successful"
pu.save()

View File

@@ -7,6 +7,7 @@ from awx.main.scheduler import TaskManager
from awx.main.scheduler.dependency_graph import DependencyGraph
from awx.main.utils import encrypt_field
from awx.main.models import WorkflowJobTemplate, JobTemplate, Job
from awx.main.models.ha import Instance, InstanceGroup
@pytest.mark.django_db
@@ -99,6 +100,48 @@ class TestJobLifeCycle:
self.run_tm(tm, expect_schedule=[mock.call()])
wfjts[0].refresh_from_db()
@pytest.fixture
def control_instance(self):
'''Control instance in the controlplane automatic IG'''
ig = InstanceGroup.objects.create(name='controlplane')
inst = Instance.objects.create(hostname='control-1', node_type='control', capacity=500)
ig.instances.add(inst)
return inst
@pytest.fixture
def execution_instance(self):
'''Execution node in the automatic default IG'''
ig = InstanceGroup.objects.create(name='default')
inst = Instance.objects.create(hostname='receptor-1', node_type='execution', capacity=500)
ig.instances.add(inst)
return inst
def test_control_and_execution_instance(self, project, system_job_template, job_template, inventory_source, control_instance, execution_instance):
assert Instance.objects.count() == 2
pu = project.create_unified_job()
sj = system_job_template.create_unified_job()
job = job_template.create_unified_job()
inv_update = inventory_source.create_unified_job()
all_ujs = (pu, sj, job, inv_update)
for uj in all_ujs:
uj.signal_start()
tm = TaskManager()
self.run_tm(tm)
for uj in all_ujs:
uj.refresh_from_db()
assert uj.status == 'waiting'
for uj in (pu, sj): # control plane jobs
assert uj.capacity_type == 'control'
assert [uj.execution_node, uj.controller_node] == [control_instance.hostname, control_instance.hostname], uj
for uj in (job, inv_update): # user-space jobs
assert uj.capacity_type == 'execution'
assert [uj.execution_node, uj.controller_node] == [execution_instance.hostname, control_instance.hostname], uj
@pytest.mark.django_db
def test_single_jt_multi_job_launch_blocks_last(default_instance_group, job_template_factory, mocker):
@@ -169,9 +212,9 @@ def test_multi_jt_capacity_blocking(default_instance_group, job_template_factory
@pytest.mark.django_db
def test_single_job_dependencies_project_launch(default_instance_group, job_template_factory, mocker):
def test_single_job_dependencies_project_launch(controlplane_instance_group, job_template_factory, mocker):
objects = job_template_factory('jt', organization='org1', project='proj', inventory='inv', credential='cred', jobs=["job_should_start"])
instance = default_instance_group.instances.all()[0]
instance = controlplane_instance_group.instances.all()[0]
j = objects.jobs["job_should_start"]
j.status = 'pending'
j.save()
@@ -188,12 +231,12 @@ def test_single_job_dependencies_project_launch(default_instance_group, job_temp
mock_pu.assert_called_once_with(j)
pu = [x for x in p.project_updates.all()]
assert len(pu) == 1
TaskManager.start_task.assert_called_once_with(pu[0], default_instance_group, [j], instance)
TaskManager.start_task.assert_called_once_with(pu[0], controlplane_instance_group, [j], instance)
pu[0].status = "successful"
pu[0].save()
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
TaskManager().schedule()
TaskManager.start_task.assert_called_once_with(j, default_instance_group, [], instance)
TaskManager.start_task.assert_called_once_with(j, controlplane_instance_group, [], instance)
@pytest.mark.django_db
@@ -254,8 +297,8 @@ def test_job_dependency_with_already_updated(default_instance_group, job_templat
@pytest.mark.django_db
def test_shared_dependencies_launch(default_instance_group, job_template_factory, mocker, inventory_source_factory):
instance = default_instance_group.instances.all()[0]
def test_shared_dependencies_launch(controlplane_instance_group, job_template_factory, mocker, inventory_source_factory):
instance = controlplane_instance_group.instances.all()[0]
objects = job_template_factory('jt', organization='org1', project='proj', inventory='inv', credential='cred', jobs=["first_job", "second_job"])
j1 = objects.jobs["first_job"]
j1.status = 'pending'
@@ -283,7 +326,7 @@ def test_shared_dependencies_launch(default_instance_group, job_template_factory
pu = p.project_updates.first()
iu = ii.inventory_updates.first()
TaskManager.start_task.assert_has_calls(
[mock.call(iu, default_instance_group, [j1, j2, pu], instance), mock.call(pu, default_instance_group, [j1, j2, iu], instance)]
[mock.call(iu, controlplane_instance_group, [j1, j2, pu], instance), mock.call(pu, controlplane_instance_group, [j1, j2, iu], instance)]
)
pu.status = "successful"
pu.finished = pu.created + timedelta(seconds=1)
@@ -293,12 +336,12 @@ def test_shared_dependencies_launch(default_instance_group, job_template_factory
iu.save()
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
TaskManager().schedule()
TaskManager.start_task.assert_called_once_with(j1, default_instance_group, [], instance)
TaskManager.start_task.assert_called_once_with(j1, controlplane_instance_group, [], instance)
j1.status = "successful"
j1.save()
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
TaskManager().schedule()
TaskManager.start_task.assert_called_once_with(j2, default_instance_group, [], instance)
TaskManager.start_task.assert_called_once_with(j2, controlplane_instance_group, [], instance)
pu = [x for x in p.project_updates.all()]
iu = [x for x in ii.inventory_updates.all()]
assert len(pu) == 1

View File

@@ -0,0 +1,26 @@
import pytest
from django.test.utils import override_settings
from awx.api.versioning import reverse
@pytest.mark.django_db
def test_change_400_error_log(caplog, post, admin_user):
with override_settings(API_400_ERROR_LOG_FORMAT='Test'):
post(url=reverse('api:setting_logging_test'), data={}, user=admin_user, expect=409)
assert 'Test' in caplog.text
@pytest.mark.django_db
def test_bad_400_error_log(caplog, post, admin_user):
with override_settings(API_400_ERROR_LOG_FORMAT="Not good {junk}"):
post(url=reverse('api:setting_logging_test'), data={}, user=admin_user, expect=409)
assert "Unable to format API_400_ERROR_LOG_FORMAT setting, defaulting log message: 'junk'" in caplog.text
assert 'status 409 received by user admin attempting to access /api/v2/settings/logging/test/ from 127.0.0.1' in caplog.text
@pytest.mark.django_db
def test_custom_400_error_log(caplog, post, admin_user):
with override_settings(API_400_ERROR_LOG_FORMAT="{status_code} {error}"):
post(url=reverse('api:setting_logging_test'), data={}, user=admin_user, expect=409)
assert '409 Logging not enabled' in caplog.text

View File

@@ -5,7 +5,7 @@ from awx.api.versioning import reverse
from awx.main.utils import decrypt_field
from awx.main.models.workflow import WorkflowJobTemplate, WorkflowJobTemplateNode, WorkflowApprovalTemplate
from awx.main.models.jobs import JobTemplate
from awx.main.tasks import deep_copy_model_obj
from awx.main.tasks.system import deep_copy_model_obj
@pytest.mark.django_db

View File

@@ -0,0 +1,19 @@
import pytest
# AWX
from awx.main.ha import is_ha_environment
from awx.main.models.ha import Instance
@pytest.mark.django_db
def test_multiple_instances():
for i in range(2):
Instance.objects.create(hostname=f'foo{i}', node_type='hybrid')
assert is_ha_environment()
@pytest.mark.django_db
def test_db_localhost():
Instance.objects.create(hostname='foo', node_type='hybrid')
Instance.objects.create(hostname='bar', node_type='execution')
assert is_ha_environment() is False

View File

@@ -2,8 +2,9 @@ import pytest
from unittest import mock
from awx.main.models import AdHocCommand, InventoryUpdate, JobTemplate, ProjectUpdate
from awx.main.models.activity_stream import ActivityStream
from awx.main.models.ha import Instance, InstanceGroup
from awx.main.tasks import apply_cluster_membership_policies
from awx.main.tasks.system import apply_cluster_membership_policies
from awx.api.versioning import reverse
from django.utils.timezone import now
@@ -68,22 +69,24 @@ class TestPolicyTaskScheduling:
@pytest.mark.django_db
def test_instance_dup(org_admin, organization, project, instance_factory, instance_group_factory, get, system_auditor):
def test_instance_dup(org_admin, organization, project, instance_factory, instance_group_factory, get, system_auditor, instance):
i1 = instance_factory("i1")
i2 = instance_factory("i2")
i3 = instance_factory("i3")
ig_all = instance_group_factory("all", instances=[i1, i2, i3])
ig_dup = instance_group_factory("duplicates", instances=[i1])
project.organization.instance_groups.add(ig_all, ig_dup)
actual_num_instances = Instance.objects.active_count()
actual_num_instances = Instance.objects.count()
list_response = get(reverse('api:instance_list'), user=system_auditor)
api_num_instances_auditor = list(list_response.data.items())[0][1]
list_response2 = get(reverse('api:instance_list'), user=org_admin)
api_num_instances_oa = list(list_response2.data.items())[0][1]
assert actual_num_instances == api_num_instances_auditor
# Note: The org_admin will not see the default 'tower' node because it is not in it's group, as expected
assert api_num_instances_auditor == actual_num_instances
# Note: The org_admin will not see the default 'tower' node
# (instance fixture) because it is not in its group, as expected
assert api_num_instances_oa == (actual_num_instances - 1)
@@ -94,7 +97,13 @@ def test_policy_instance_few_instances(instance_factory, instance_group_factory)
ig_2 = instance_group_factory("ig2", percentage=25)
ig_3 = instance_group_factory("ig3", percentage=25)
ig_4 = instance_group_factory("ig4", percentage=25)
count = ActivityStream.objects.count()
apply_cluster_membership_policies()
# running apply_cluster_membership_policies shouldn't spam the activity stream
assert ActivityStream.objects.count() == count
assert len(ig_1.instances.all()) == 1
assert i1 in ig_1.instances.all()
assert len(ig_2.instances.all()) == 1
@@ -103,8 +112,12 @@ def test_policy_instance_few_instances(instance_factory, instance_group_factory)
assert i1 in ig_3.instances.all()
assert len(ig_4.instances.all()) == 1
assert i1 in ig_4.instances.all()
i2 = instance_factory("i2")
count += 1
apply_cluster_membership_policies()
assert ActivityStream.objects.count() == count
assert len(ig_1.instances.all()) == 1
assert i1 in ig_1.instances.all()
assert len(ig_2.instances.all()) == 1
@@ -243,6 +256,41 @@ def test_policy_instance_list_explicitly_pinned(instance_factory, instance_group
assert set(ig_2.instances.all()) == set([i2])
@pytest.mark.django_db
def test_control_plane_policy_exception(controlplane_instance_group):
controlplane_instance_group.policy_instance_percentage = 100
controlplane_instance_group.policy_instance_minimum = 2
controlplane_instance_group.save()
Instance.objects.create(hostname='foo-1', node_type='execution')
apply_cluster_membership_policies()
assert 'foo-1' not in [inst.hostname for inst in controlplane_instance_group.instances.all()]
@pytest.mark.django_db
def test_normal_instance_group_policy_exception():
ig = InstanceGroup.objects.create(name='bar', policy_instance_percentage=100, policy_instance_minimum=2)
Instance.objects.create(hostname='foo-1', node_type='control')
apply_cluster_membership_policies()
assert 'foo-1' not in [inst.hostname for inst in ig.instances.all()]
@pytest.mark.django_db
def test_percentage_as_fraction_of_execution_nodes():
"""
If an instance requests 50 percent of instances, then those should be 50 percent
of available execution nodes (1 out of 2), as opposed to 50 percent
of all available nodes (2 out of 4) which include unusable control nodes
"""
ig = InstanceGroup.objects.create(name='bar', policy_instance_percentage=50)
for i in range(2):
Instance.objects.create(hostname=f'foo-{i}', node_type='control')
for i in range(2):
Instance.objects.create(hostname=f'bar-{i}', node_type='execution')
apply_cluster_membership_policies()
assert ig.instances.count() == 1
assert ig.instances.first().hostname.startswith('bar-')
@pytest.mark.django_db
def test_basic_instance_group_membership(instance_group_factory, default_instance_group, job_factory):
j = job_factory()
@@ -266,6 +314,12 @@ def test_inherited_instance_group_membership(instance_group_factory, default_ins
assert default_instance_group not in j.preferred_instance_groups
@pytest.mark.django_db
def test_global_instance_groups_as_defaults(controlplane_instance_group, default_instance_group, job_factory):
j = job_factory()
assert j.preferred_instance_groups == [default_instance_group, controlplane_instance_group]
@pytest.mark.django_db
def test_mixed_group_membership(instance_factory, instance_group_factory):
for i in range(5):
@@ -289,6 +343,23 @@ def test_instance_group_capacity(instance_factory, instance_group_factory):
assert ig_single.capacity == 100
@pytest.mark.django_db
def test_health_check_clears_errors():
instance = Instance.objects.create(hostname='foo-1', enabled=True, capacity=0, errors='something went wrong')
data = dict(version='ansible-runner-4.2', cpu=782, memory=int(39e9), uuid='asdfasdfasdfasdfasdf', errors='')
instance.save_health_data(**data)
for k, v in data.items():
assert getattr(instance, k) == v
@pytest.mark.django_db
def test_health_check_oh_no():
instance = Instance.objects.create(hostname='foo-2', enabled=True, capacity=52, cpu=8, memory=int(40e9))
instance.save_health_data('', 0, 0, errors='This it not a real instance!')
assert instance.capacity == instance.cpu_capacity == 0
assert instance.errors == 'This it not a real instance!'
@pytest.mark.django_db
class TestInstanceGroupOrdering:
def test_ad_hoc_instance_groups(self, instance_group_factory, inventory, default_instance_group):
@@ -314,15 +385,15 @@ class TestInstanceGroupOrdering:
# API does not allow setting IGs on inventory source, so ignore those
assert iu.preferred_instance_groups == [ig_inv, ig_org]
def test_project_update_instance_groups(self, instance_group_factory, project, default_instance_group):
def test_project_update_instance_groups(self, instance_group_factory, project, controlplane_instance_group):
pu = ProjectUpdate.objects.create(project=project, organization=project.organization)
assert pu.preferred_instance_groups == [default_instance_group]
ig_org = instance_group_factory("OrgIstGrp", [default_instance_group.instances.first()])
ig_tmp = instance_group_factory("TmpIstGrp", [default_instance_group.instances.first()])
assert pu.preferred_instance_groups == [controlplane_instance_group]
ig_org = instance_group_factory("OrgIstGrp", [controlplane_instance_group.instances.first()])
ig_tmp = instance_group_factory("TmpIstGrp", [controlplane_instance_group.instances.first()])
project.organization.instance_groups.add(ig_org)
assert pu.preferred_instance_groups == [ig_org]
assert pu.preferred_instance_groups == [ig_org, controlplane_instance_group]
project.instance_groups.add(ig_tmp)
assert pu.preferred_instance_groups == [ig_tmp, ig_org]
assert pu.preferred_instance_groups == [ig_tmp, ig_org, controlplane_instance_group]
def test_job_instance_groups(self, instance_group_factory, inventory, project, default_instance_group):
jt = JobTemplate.objects.create(inventory=inventory, project=project)

View File

@@ -5,7 +5,7 @@ import json
import re
from collections import namedtuple
from awx.main.tasks import RunInventoryUpdate
from awx.main.tasks.jobs import RunInventoryUpdate
from awx.main.models import InventorySource, Credential, CredentialType, UnifiedJob, ExecutionEnvironment
from awx.main.constants import CLOUD_PROVIDERS, STANDARD_INVENTORY_UPDATE_ENV
from awx.main.tests import data
@@ -257,6 +257,6 @@ def test_inventory_update_injected_content(this_kind, inventory, fake_credential
# Also do not send websocket status updates
with mock.patch.object(UnifiedJob, 'websocket_emit_status', mock.Mock()):
# The point of this test is that we replace run with assertions
with mock.patch('awx.main.tasks.AWXReceptorJob.run', substitute_run):
with mock.patch('awx.main.tasks.receptor.AWXReceptorJob.run', substitute_run):
# so this sets up everything for a run and then yields control over to substitute_run
task.run(inventory_update.pk)

View File

@@ -4,7 +4,7 @@ from unittest import mock
import json
from awx.main.models import Job, Instance, JobHostSummary, InventoryUpdate, InventorySource, Project, ProjectUpdate, SystemJob, AdHocCommand
from awx.main.tasks import cluster_node_heartbeat
from awx.main.tasks.system import cluster_node_heartbeat
from django.test.utils import override_settings
@@ -20,24 +20,27 @@ def test_orphan_unified_job_creation(instance, inventory):
@pytest.mark.django_db
@mock.patch('awx.main.utils.common.get_cpu_capacity', lambda: (2, 8))
@mock.patch('awx.main.utils.common.get_mem_capacity', lambda: (8000, 62))
@mock.patch('awx.main.tasks.system.inspect_execution_nodes', lambda *args, **kwargs: None)
@mock.patch('awx.main.models.ha.get_cpu_effective_capacity', lambda cpu: 8)
@mock.patch('awx.main.models.ha.get_mem_effective_capacity', lambda mem: 62)
def test_job_capacity_and_with_inactive_node():
i = Instance.objects.create(hostname='test-1')
with mock.patch.object(redis.client.Redis, 'ping', lambda self: True):
i.refresh_capacity()
i.save_health_data('18.0.1', 2, 8000)
assert i.enabled is True
assert i.capacity_adjustment == 1.0
assert i.capacity == 62
i.enabled = False
i.save()
with override_settings(CLUSTER_HOST_ID=i.hostname):
cluster_node_heartbeat()
with mock.patch.object(redis.client.Redis, 'ping', lambda self: True):
cluster_node_heartbeat()
i = Instance.objects.get(id=i.id)
assert i.capacity == 0
@pytest.mark.django_db
@mock.patch('awx.main.utils.common.get_cpu_capacity', lambda: (2, 8))
@mock.patch('awx.main.utils.common.get_mem_capacity', lambda: (8000, 62))
@mock.patch('awx.main.models.ha.get_cpu_effective_capacity', lambda cpu: 8)
@mock.patch('awx.main.models.ha.get_mem_effective_capacity', lambda mem: 62)
def test_job_capacity_with_redis_disabled():
i = Instance.objects.create(hostname='test-1')
@@ -45,7 +48,7 @@ def test_job_capacity_with_redis_disabled():
raise redis.ConnectionError()
with mock.patch.object(redis.client.Redis, 'ping', _raise):
i.refresh_capacity()
i.local_health_check()
assert i.capacity == 0

View File

@@ -9,6 +9,8 @@ try:
except ImportError:
from pip.req import parse_requirements
from pip._internal.req.constructors import parse_req_from_line
def test_python_and_js_licenses():
def index_licenses(path):
@@ -48,21 +50,30 @@ def test_python_and_js_licenses():
def read_api_requirements(path):
ret = {}
skip_pbr_license_check = False
for req_file in ['requirements.txt', 'requirements_git.txt']:
fname = '%s/%s' % (path, req_file)
for reqt in parse_requirements(fname, session=''):
name = reqt.name
version = str(reqt.specifier)
parsed_requirement = parse_req_from_line(reqt.requirement, None)
name = parsed_requirement.requirement.name
version = str(parsed_requirement.requirement.specifier)
if version.startswith('=='):
version = version[2:]
if reqt.link:
(name, version) = reqt.link.filename.split('@', 1)
if parsed_requirement.link:
if str(parsed_requirement.link).startswith(('http://', 'https://')):
(name, version) = str(parsed_requirement.requirement).split('==', 1)
else:
(name, version) = parsed_requirement.link.filename.split('@', 1)
if name.endswith('.git'):
name = name[:-4]
if name == 'receptor':
name = 'receptorctl'
if name == 'ansible-runner':
skip_pbr_license_check = True
ret[name] = {'name': name, 'version': version}
if 'pbr' in ret and skip_pbr_license_check:
del ret['pbr']
return ret
def read_ui_requirements(path):

View File

@@ -2,8 +2,9 @@ import pytest
from unittest import mock
import os
from awx.main.tasks import RunProjectUpdate, RunInventoryUpdate
from awx.main.models import ProjectUpdate, InventoryUpdate, InventorySource
from awx.main.tasks.jobs import RunProjectUpdate, RunInventoryUpdate
from awx.main.tasks.system import execution_node_health_check
from awx.main.models import ProjectUpdate, InventoryUpdate, InventorySource, Instance
@pytest.fixture
@@ -15,6 +16,15 @@ def scm_revision_file(tmpdir_factory):
return os.path.join(revision_file.dirname, 'revision.txt')
@pytest.mark.django_db
@pytest.mark.parametrize('node_type', ('control', 'hybrid'))
def test_no_worker_info_on_AWX_nodes(node_type):
hostname = 'us-south-3-compute.invalid'
Instance.objects.create(hostname=hostname, node_type=node_type)
with pytest.raises(RuntimeError):
execution_node_health_check(hostname)
@pytest.mark.django_db
class TestDependentInventoryUpdate:
def test_dependent_inventory_updates_is_called(self, scm_inventory_source, scm_revision_file):
@@ -40,7 +50,7 @@ class TestDependentInventoryUpdate:
scm_inventory_source.scm_last_revision = ''
proj_update = ProjectUpdate.objects.create(project=scm_inventory_source.source_project)
with mock.patch.object(RunInventoryUpdate, 'run') as iu_run_mock:
with mock.patch('awx.main.tasks.create_partition'):
with mock.patch('awx.main.tasks.jobs.create_partition'):
task._update_dependent_inventories(proj_update, [scm_inventory_source])
assert InventoryUpdate.objects.count() == 1
inv_update = InventoryUpdate.objects.first()
@@ -64,7 +74,7 @@ class TestDependentInventoryUpdate:
ProjectUpdate.objects.all().update(cancel_flag=True)
with mock.patch.object(RunInventoryUpdate, 'run') as iu_run_mock:
with mock.patch('awx.main.tasks.create_partition'):
with mock.patch('awx.main.tasks.jobs.create_partition'):
iu_run_mock.side_effect = user_cancels_project
task._update_dependent_inventories(proj_update, [is1, is2])
# Verify that it bails after 1st update, detecting a cancel

View File

@@ -1,15 +1,25 @@
import pytest
from unittest import mock
from unittest.mock import Mock
from decimal import Decimal
from awx.main.models import (
InstanceGroup,
)
from awx.main.models import InstanceGroup, Instance
@pytest.mark.parametrize('capacity_adjustment', [0.0, 0.25, 0.5, 0.75, 1, 1.5, 3])
def test_capacity_adjustment_no_save(capacity_adjustment):
inst = Instance(hostname='test-host', capacity_adjustment=Decimal(capacity_adjustment), capacity=0, cpu_capacity=10, mem_capacity=1000)
assert inst.capacity == 0
assert inst.capacity_adjustment == capacity_adjustment # sanity
inst.set_capacity_value()
assert inst.capacity > 0
assert inst.capacity == (float(inst.capacity_adjustment) * abs(inst.mem_capacity - inst.cpu_capacity) + min(inst.mem_capacity, inst.cpu_capacity))
def T(impact):
j = mock.Mock(spec_set=['task_impact'])
j = mock.Mock(spec_set=['task_impact', 'capacity_type'])
j.task_impact = impact
j.capacity_type = 'execution'
return j
@@ -26,11 +36,13 @@ def Is(param):
inst = Mock()
inst.capacity = capacity
inst.jobs_running = jobs_running
inst.node_type = 'execution'
instances.append(inst)
else:
for i in param:
inst = Mock()
inst.remaining_capacity = i
inst.node_type = 'execution'
instances.append(inst)
return instances
@@ -77,3 +89,19 @@ class TestInstanceGroup(object):
assert ig.find_largest_idle_instance(instances_online_only) is None, reason
else:
assert ig.find_largest_idle_instance(instances_online_only) == instances[instance_fit_index], reason
def test_cleanup_params_defaults():
inst = Instance(hostname='foobar')
assert inst.get_cleanup_task_kwargs(exclude_strings=['awx_423_']) == {'exclude_strings': ['awx_423_'], 'file_pattern': '/tmp/awx_*_*'}
def test_cleanup_params_for_image_cleanup():
inst = Instance(hostname='foobar')
# see CLI conversion in awx.main.tests.unit.utils.test_receptor
assert inst.get_cleanup_task_kwargs(file_pattern='', remove_images=['quay.invalid/foo/bar'], image_prune=True) == {
'file_pattern': '',
'process_isolation_executable': 'podman',
'remove_images': ['quay.invalid/foo/bar'],
'image_prune': True,
}

View File

@@ -7,8 +7,11 @@ import awx.main.notifications.rocketchat_backend as rocketchat_backend
def test_send_messages():
with mock.patch('awx.main.notifications.rocketchat_backend.requests') as requests_mock:
with mock.patch('awx.main.notifications.rocketchat_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.rocketchat_backend.get_awx_http_client_headers'
) as version_mock:
requests_mock.post.return_value.status_code = 201
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = rocketchat_backend.RocketChatBackend()
message = EmailMessage(
'test subject',
@@ -23,7 +26,12 @@ def test_send_messages():
message,
]
)
requests_mock.post.assert_called_once_with('http://example.com', data='{"text": "test subject"}', verify=True)
requests_mock.post.assert_called_once_with(
'http://example.com',
data='{"text": "test subject"}',
headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'},
verify=True,
)
assert sent_messages == 1
@@ -84,8 +92,11 @@ def test_send_messages_with_icon_url():
def test_send_messages_with_no_verify_ssl():
with mock.patch('awx.main.notifications.rocketchat_backend.requests') as requests_mock:
with mock.patch('awx.main.notifications.rocketchat_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.rocketchat_backend.get_awx_http_client_headers'
) as version_mock:
requests_mock.post.return_value.status_code = 201
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = rocketchat_backend.RocketChatBackend(rocketchat_no_verify_ssl=True)
message = EmailMessage(
'test subject',
@@ -100,5 +111,10 @@ def test_send_messages_with_no_verify_ssl():
message,
]
)
requests_mock.post.assert_called_once_with('http://example.com', data='{"text": "test subject"}', verify=False)
requests_mock.post.assert_called_once_with(
'http://example.com',
data='{"text": "test subject"}',
headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'},
verify=False,
)
assert sent_messages == 1

View File

@@ -0,0 +1,75 @@
import pytest
from unittest import mock
from django.core.mail.message import EmailMessage
import awx.main.notifications.slack_backend as slack_backend
def test_send_messages():
with mock.patch('awx.main.notifications.slack_backend.WebClient') as slack_sdk_mock:
WebClient_mock = slack_sdk_mock.return_value
WebClient_mock.chat_postMessage.return_value = {'ok': True}
backend = slack_backend.SlackBackend('slack_access_token')
message = EmailMessage(
'test subject',
'test body',
[],
[
'#random',
],
)
sent_messages = backend.send_messages(
[
message,
]
)
WebClient_mock.chat_postMessage.assert_called_once_with(channel='random', thread_ts=None, as_user=True, text='test subject')
assert sent_messages == 1
def test_send_messages_with_color():
with mock.patch('awx.main.notifications.slack_backend.WebClient') as slack_sdk_mock:
WebClient_mock = slack_sdk_mock.return_value
WebClient_mock.chat_postMessage.return_value = {'ok': True}
backend = slack_backend.SlackBackend('slack_access_token', hex_color='#006699')
message = EmailMessage(
'test subject',
'test body',
[],
[
'#random',
],
)
sent_messages = backend.send_messages(
[
message,
]
)
WebClient_mock.chat_postMessage.assert_called_once_with(
channel='random', as_user=True, thread_ts=None, attachments=[{'color': '#006699', 'text': 'test subject'}]
)
assert sent_messages == 1
def test_send_messages_fail():
with mock.patch('awx.main.notifications.slack_backend.WebClient') as slack_sdk_mock, pytest.raises(RuntimeError, match=r'.*not_in_channel.*'):
WebClient_mock = slack_sdk_mock.return_value
WebClient_mock.chat_postMessage.return_value = {'ok': False, 'error': 'not_in_channel'}
backend = slack_backend.SlackBackend('slack_access_token')
message = EmailMessage(
'test subject',
'test body',
[],
[
'#not_existing',
],
)
sent_messages = backend.send_messages(
[
message,
]
)
WebClient_mock.chat_postMessage.assert_called_once_with(channel='not_existing', as_user=True, text='test subject')
assert sent_messages == 0

View File

@@ -0,0 +1,221 @@
import json
from unittest import mock
from django.core.mail.message import EmailMessage
import awx.main.notifications.webhook_backend as webhook_backend
def test_send_messages_as_POST():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
requests_mock.post.return_value.status_code = 200
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = webhook_backend.WebhookBackend('POST', None)
message = EmailMessage(
'test subject',
{'text': 'test body'},
[],
[
'http://example.com',
],
)
sent_messages = backend.send_messages(
[
message,
]
)
requests_mock.post.assert_called_once_with(
'http://example.com',
auth=None,
data=json.dumps({'text': 'test body'}, ensure_ascii=False).encode('utf-8'),
headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'},
verify=True,
)
assert sent_messages == 1
def test_send_messages_as_PUT():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
requests_mock.put.return_value.status_code = 200
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = webhook_backend.WebhookBackend('PUT', None)
message = EmailMessage(
'test subject 2',
{'text': 'test body 2'},
[],
[
'http://example.com',
],
)
sent_messages = backend.send_messages(
[
message,
]
)
requests_mock.put.assert_called_once_with(
'http://example.com',
auth=None,
data=json.dumps({'text': 'test body 2'}, ensure_ascii=False).encode('utf-8'),
headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'},
verify=True,
)
assert sent_messages == 1
def test_send_messages_with_username():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
requests_mock.post.return_value.status_code = 200
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = webhook_backend.WebhookBackend('POST', None, username='userstring')
message = EmailMessage(
'test subject',
{'text': 'test body'},
[],
[
'http://example.com',
],
)
sent_messages = backend.send_messages(
[
message,
]
)
requests_mock.post.assert_called_once_with(
'http://example.com',
auth=('userstring', None),
data=json.dumps({'text': 'test body'}, ensure_ascii=False).encode('utf-8'),
headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'},
verify=True,
)
assert sent_messages == 1
def test_send_messages_with_password():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
requests_mock.post.return_value.status_code = 200
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = webhook_backend.WebhookBackend('POST', None, password='passwordstring')
message = EmailMessage(
'test subject',
{'text': 'test body'},
[],
[
'http://example.com',
],
)
sent_messages = backend.send_messages(
[
message,
]
)
requests_mock.post.assert_called_once_with(
'http://example.com',
auth=(None, 'passwordstring'),
data=json.dumps({'text': 'test body'}, ensure_ascii=False).encode('utf-8'),
headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'},
verify=True,
)
assert sent_messages == 1
def test_send_messages_with_username_and_password():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
requests_mock.post.return_value.status_code = 200
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = webhook_backend.WebhookBackend('POST', None, username='userstring', password='passwordstring')
message = EmailMessage(
'test subject',
{'text': 'test body'},
[],
[
'http://example.com',
],
)
sent_messages = backend.send_messages(
[
message,
]
)
requests_mock.post.assert_called_once_with(
'http://example.com',
auth=('userstring', 'passwordstring'),
data=json.dumps({'text': 'test body'}, ensure_ascii=False).encode('utf-8'),
headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'},
verify=True,
)
assert sent_messages == 1
def test_send_messages_with_no_verify_ssl():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
requests_mock.post.return_value.status_code = 200
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = webhook_backend.WebhookBackend('POST', None, disable_ssl_verification=True)
message = EmailMessage(
'test subject',
{'text': 'test body'},
[],
[
'http://example.com',
],
)
sent_messages = backend.send_messages(
[
message,
]
)
requests_mock.post.assert_called_once_with(
'http://example.com',
auth=None,
data=json.dumps({'text': 'test body'}, ensure_ascii=False).encode('utf-8'),
headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'},
verify=False,
)
assert sent_messages == 1
def test_send_messages_with_additional_headers():
with mock.patch('awx.main.notifications.webhook_backend.requests') as requests_mock, mock.patch(
'awx.main.notifications.webhook_backend.get_awx_http_client_headers'
) as version_mock:
requests_mock.post.return_value.status_code = 200
version_mock.return_value = {'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}
backend = webhook_backend.WebhookBackend('POST', {'X-Test-Header1': 'test-content-1', 'X-Test-Header2': 'test-content-2'})
message = EmailMessage(
'test subject',
{'text': 'test body'},
[],
[
'http://example.com',
],
)
sent_messages = backend.send_messages(
[
message,
]
)
requests_mock.post.assert_called_once_with(
'http://example.com',
auth=None,
data=json.dumps({'text': 'test body'}, ensure_ascii=False).encode('utf-8'),
headers={
'Content-Type': 'application/json',
'User-Agent': 'AWX 0.0.1.dev (open)',
'X-Test-Header1': 'test-content-1',
'X-Test-Header2': 'test-content-2',
},
verify=True,
)
assert sent_messages == 1

View File

@@ -7,7 +7,7 @@ from datetime import timedelta
@pytest.mark.parametrize(
"job_name,function_path",
[
('tower_scheduler', 'awx.main.tasks.awx_periodic_scheduler'),
('tower_scheduler', 'awx.main.tasks.system.awx_periodic_scheduler'),
],
)
def test_CELERYBEAT_SCHEDULE(mocker, job_name, function_path):

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