mirror of
https://github.com/ansible/awx.git
synced 2026-02-07 04:28:23 -03:30
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa4ca300f5 | ||
|
|
8d8aadb193 | ||
|
|
3194690e5f | ||
|
|
6cc3ac2e99 | ||
|
|
c4c1b9799e | ||
|
|
9f691a048d | ||
|
|
da68e613e3 | ||
|
|
b9a9d645de | ||
|
|
7032927ba3 | ||
|
|
e62035fa5e | ||
|
|
5fe6e75255 | ||
|
|
83372d6f03 | ||
|
|
7c53bf3681 | ||
|
|
e40e646211 | ||
|
|
b913d8411a | ||
|
|
2017504c51 | ||
|
|
16848e9154 | ||
|
|
ee59ac957a | ||
|
|
6b7d712f9f |
@@ -2,6 +2,10 @@
|
||||
|
||||
This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/<version>`.
|
||||
|
||||
# 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
|
||||
|
||||
13
README.md
13
README.md
@@ -14,20 +14,20 @@ Contributing
|
||||
------------
|
||||
|
||||
- Refer to the [Contributing guide](./CONTRIBUTING.md) to get started developing, testing, and building AWX.
|
||||
- All code submissions are done through pull requests against the `devel` branch.
|
||||
- All contributors must use git commit --signoff for any commit to be merged, and agree that usage of --signoff constitutes agreement with the terms of [DCO 1.1](./DCO_1_1.md)
|
||||
- Take care to make sure no merge commits are in the submission, and use `git rebase` vs `git merge` for this reason.
|
||||
- If submitting a large code change, it's a good idea to join the `#ansible-awx` channel on irc.freenode.net, and talk about what you would like to do or add first. This not only helps everyone know what's going on, it also helps save time and effort, if the community decides some changes are needed.
|
||||
- All code submissions are made through pull requests against the `devel` branch.
|
||||
- All contributors must use git commit --signoff for any commit to be merged and agree that usage of --signoff constitutes agreement with the terms of [DCO 1.1](./DCO_1_1.md)
|
||||
- Take care to make sure no merge commits are in the submission, and use `git rebase` vs. `git merge` for this reason.
|
||||
- If submitting a large code change, it's a good idea to join the `#ansible-awx` channel on irc.freenode.net and talk about what you would like to do or add first. This not only helps everyone know what's going on, but it also helps save time and effort if the community decides some changes are needed.
|
||||
|
||||
Reporting Issues
|
||||
----------------
|
||||
|
||||
If you're experiencing a problem that you feel is a bug in AWX, or have ideas for how to improve AWX, we encourage you to open an issue, and share your feedback. But before opening a new issue, we ask that you please take a look at our [Issues guide](./ISSUES.md).
|
||||
If you're experiencing a problem that you feel is a bug in AWX or have ideas for improving AWX, we encourage you to open an issue and share your feedback. But before opening a new issue, we ask that you please take a look at our [Issues guide](./ISSUES.md).
|
||||
|
||||
Code of Conduct
|
||||
---------------
|
||||
|
||||
We ask all of our community members and contributors to adhere to the [Ansible code of conduct](http://docs.ansible.com/ansible/latest/community/code_of_conduct.html). If you have questions, or need assistance, please reach out to our community team at [codeofconduct@ansible.com](mailto:codeofconduct@ansible.com)
|
||||
We ask all of our community members and contributors to adhere to the [Ansible code of conduct](http://docs.ansible.com/ansible/latest/community/code_of_conduct.html). If you have questions or need assistance, please reach out to our community team at [codeofconduct@ansible.com](mailto:codeofconduct@ansible.com)
|
||||
|
||||
Get Involved
|
||||
------------
|
||||
@@ -41,4 +41,3 @@ License
|
||||
-------
|
||||
|
||||
[Apache v2](./LICENSE.md)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from awx.main.models import (
|
||||
ActivityStream,
|
||||
Inventory,
|
||||
Host,
|
||||
Project,
|
||||
JobTemplate,
|
||||
WorkflowJobTemplate,
|
||||
@@ -98,6 +99,7 @@ class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPI
|
||||
organization__id=org_id).count()
|
||||
org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).filter(
|
||||
organization__id=org_id).count()
|
||||
org_counts['hosts'] = Host.objects.org_active_count(org_id)
|
||||
|
||||
full_context['related_field_counts'] = {}
|
||||
full_context['related_field_counts'][org_id] = org_counts
|
||||
|
||||
@@ -81,10 +81,17 @@ User.add_to_class('accessible_objects', user_accessible_objects)
|
||||
|
||||
|
||||
def enforce_bigint_pk_migration():
|
||||
#
|
||||
# NOTE: this function is not actually in use anymore,
|
||||
# but has been intentionally kept for historical purposes,
|
||||
# and to serve as an illustration if we ever need to perform
|
||||
# bulk modification/migration of event data in the future.
|
||||
#
|
||||
# see: https://github.com/ansible/awx/issues/6010
|
||||
# look at all the event tables and verify that they have been fully migrated
|
||||
# from the *old* int primary key table to the replacement bigint table
|
||||
# if not, attempt to migrate them in the background
|
||||
#
|
||||
for tblname in (
|
||||
'main_jobevent', 'main_inventoryupdateevent',
|
||||
'main_projectupdateevent', 'main_adhoccommandevent',
|
||||
|
||||
@@ -357,7 +357,7 @@ class JobNotificationMixin(object):
|
||||
'url': 'https://towerhost/#/jobs/playbook/1010',
|
||||
'approval_status': 'approved',
|
||||
'approval_node_name': 'Approve Me',
|
||||
'workflow_url': 'https://towerhost/#/workflows/1010',
|
||||
'workflow_url': 'https://towerhost/#/jobs/workflow/1010',
|
||||
'job_metadata': """{'url': 'https://towerhost/$/jobs/playbook/13',
|
||||
'traceback': '',
|
||||
'status': 'running',
|
||||
|
||||
@@ -620,7 +620,7 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio
|
||||
return reverse('api:workflow_job_detail', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
def get_ui_url(self):
|
||||
return urljoin(settings.TOWER_URL_BASE, '/#/workflows/{}'.format(self.pk))
|
||||
return urljoin(settings.TOWER_URL_BASE, '/#/jobs/workflow/{}'.format(self.pk))
|
||||
|
||||
def notification_data(self):
|
||||
result = super(WorkflowJob, self).notification_data()
|
||||
@@ -752,7 +752,7 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin):
|
||||
return None
|
||||
|
||||
def get_ui_url(self):
|
||||
return urljoin(settings.TOWER_URL_BASE, '/#/workflows/{}'.format(self.workflow_job.id))
|
||||
return urljoin(settings.TOWER_URL_BASE, '/#/jobs/workflow/{}'.format(self.workflow_job.id))
|
||||
|
||||
def _get_parent_field_name(self):
|
||||
return 'workflow_approval_template'
|
||||
@@ -840,7 +840,7 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin):
|
||||
return (msg, body)
|
||||
|
||||
def context(self, approval_status):
|
||||
workflow_url = urljoin(settings.TOWER_URL_BASE, '/#/workflows/{}'.format(self.workflow_job.id))
|
||||
workflow_url = urljoin(settings.TOWER_URL_BASE, '/#/jobs/workflow/{}'.format(self.workflow_job.id))
|
||||
return {'approval_status': approval_status,
|
||||
'approval_node_name': self.workflow_approval_template.name,
|
||||
'workflow_url': workflow_url,
|
||||
|
||||
@@ -60,7 +60,7 @@ from awx.main.models import (
|
||||
Inventory, InventorySource, SmartInventoryMembership,
|
||||
Job, AdHocCommand, ProjectUpdate, InventoryUpdate, SystemJob,
|
||||
JobEvent, ProjectUpdateEvent, InventoryUpdateEvent, AdHocCommandEvent, SystemJobEvent,
|
||||
build_safe_env, enforce_bigint_pk_migration
|
||||
build_safe_env
|
||||
)
|
||||
from awx.main.constants import ACTIVE_STATES
|
||||
from awx.main.exceptions import AwxTaskError, PostRunError
|
||||
@@ -138,12 +138,6 @@ def dispatch_startup():
|
||||
if Instance.objects.me().is_controller():
|
||||
awx_isolated_heartbeat()
|
||||
|
||||
# at process startup, detect the need to migrate old event records from int
|
||||
# to bigint; at *some point* in the future, once certain versions of AWX
|
||||
# and Tower fall out of use/support, we can probably just _assume_ that
|
||||
# everybody has moved to bigint, and remove this code entirely
|
||||
enforce_bigint_pk_migration()
|
||||
|
||||
# Update Tower's rsyslog.conf file based on loggins settings in the db
|
||||
reconfigure_rsyslog()
|
||||
|
||||
@@ -738,6 +732,12 @@ def update_host_smart_inventory_memberships():
|
||||
|
||||
@task(queue=get_local_queuename)
|
||||
def migrate_legacy_event_data(tblname):
|
||||
#
|
||||
# NOTE: this function is not actually in use anymore,
|
||||
# but has been intentionally kept for historical purposes,
|
||||
# and to serve as an illustration if we ever need to perform
|
||||
# bulk modification/migration of event data in the future.
|
||||
#
|
||||
if 'event' not in tblname:
|
||||
return
|
||||
with advisory_lock(f'bigint_migration_{tblname}', wait=False) as acquired:
|
||||
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
|
||||
from awx.api.versioning import reverse
|
||||
|
||||
from awx.main.models import Project
|
||||
from awx.main.models import Project, Host
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -81,6 +81,8 @@ def test_org_counts_detail_admin(resourced_organization, user, get):
|
||||
assert response.status_code == 200
|
||||
|
||||
counts = response.data['summary_fields']['related_field_counts']
|
||||
assert counts['hosts'] == 0
|
||||
counts.pop('hosts')
|
||||
assert counts == COUNTS_PRIMES
|
||||
|
||||
|
||||
@@ -93,6 +95,8 @@ def test_org_counts_detail_member(resourced_organization, user, get):
|
||||
assert response.status_code == 200
|
||||
|
||||
counts = response.data['summary_fields']['related_field_counts']
|
||||
assert counts['hosts'] == 0
|
||||
counts.pop('hosts')
|
||||
assert counts == {
|
||||
'users': COUNTS_PRIMES['users'], # Policy is that members can see other users and admins
|
||||
'admins': COUNTS_PRIMES['admins'],
|
||||
@@ -111,6 +115,7 @@ def test_org_counts_list_admin(resourced_organization, user, get):
|
||||
assert response.status_code == 200
|
||||
|
||||
counts = response.data['results'][0]['summary_fields']['related_field_counts']
|
||||
assert 'hosts' not in counts # doesn't show in list view
|
||||
assert counts == COUNTS_PRIMES
|
||||
|
||||
|
||||
@@ -123,6 +128,7 @@ def test_org_counts_list_member(resourced_organization, user, get):
|
||||
assert response.status_code == 200
|
||||
|
||||
counts = response.data['results'][0]['summary_fields']['related_field_counts']
|
||||
assert 'hosts' not in counts # doesn't show in list view
|
||||
|
||||
assert counts == {
|
||||
'users': COUNTS_PRIMES['users'], # Policy is that members can see other users and admins
|
||||
@@ -145,6 +151,7 @@ def test_new_org_zero_counts(user, post):
|
||||
|
||||
new_org_list = post_response.render().data
|
||||
counts_dict = new_org_list['summary_fields']['related_field_counts']
|
||||
assert 'hosts' not in counts_dict # doesn't show in list view
|
||||
assert counts_dict == COUNTS_ZEROS
|
||||
|
||||
|
||||
@@ -167,6 +174,19 @@ def test_two_organizations(resourced_organization, organizations, user, get):
|
||||
assert counts[org_id_zero] == COUNTS_ZEROS
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_hosts_counted(resourced_organization, user, get):
|
||||
admin_user = user('admin', True)
|
||||
assert Host.objects.org_active_count(resourced_organization.id) == 0
|
||||
resourced_organization.inventories.first().hosts.create(name='Some Host')
|
||||
assert Host.objects.org_active_count(resourced_organization.id) == 1
|
||||
response = get(reverse('api:organization_detail', kwargs={'pk': resourced_organization.pk}), admin_user)
|
||||
assert response.status_code == 200
|
||||
|
||||
counts = response.data['summary_fields']['related_field_counts']
|
||||
assert counts['hosts'] == Host.objects.org_active_count(resourced_organization.id) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_scan_JT_counted(resourced_organization, user, get):
|
||||
admin_user = user('admin', True)
|
||||
@@ -180,7 +200,10 @@ def test_scan_JT_counted(resourced_organization, user, get):
|
||||
# Test detail view
|
||||
detail_response = get(reverse('api:organization_detail', kwargs={'pk': resourced_organization.pk}), admin_user)
|
||||
assert detail_response.status_code == 200
|
||||
assert detail_response.data['summary_fields']['related_field_counts'] == counts_dict
|
||||
counts = detail_response.data['summary_fields']['related_field_counts']
|
||||
assert 'hosts' in counts
|
||||
counts.pop('hosts')
|
||||
assert counts == counts_dict
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -205,4 +228,7 @@ def test_JT_not_double_counted(resourced_organization, user, get):
|
||||
# Test detail view
|
||||
detail_response = get(reverse('api:organization_detail', kwargs={'pk': resourced_organization.pk}), admin_user)
|
||||
assert detail_response.status_code == 200
|
||||
assert detail_response.data['summary_fields']['related_field_counts'] == counts_dict
|
||||
counts = detail_response.data['summary_fields']['related_field_counts']
|
||||
assert 'hosts' in counts
|
||||
counts.pop('hosts')
|
||||
assert counts == counts_dict
|
||||
|
||||
@@ -94,7 +94,7 @@ const buildAnchor = (obj, resource, activity) => {
|
||||
break;
|
||||
}
|
||||
case 'workflow_job':
|
||||
url = `/workflows/${obj.id}/`;
|
||||
url = `/jobs/workflow/${obj.id}/`;
|
||||
break;
|
||||
case 'label':
|
||||
url = null;
|
||||
|
||||
@@ -48,10 +48,10 @@ const SettingGroup = withI18n()(
|
||||
<FormGroup
|
||||
fieldId={fieldId}
|
||||
helperTextInvalid={helperTextInvalid}
|
||||
id={`${fieldId}-field`}
|
||||
isRequired={isRequired}
|
||||
label={label}
|
||||
validated={validated}
|
||||
id={fieldId}
|
||||
labelIcon={
|
||||
<>
|
||||
<Popover
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { Formik } from 'formik';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
import {
|
||||
@@ -12,28 +14,38 @@ import {
|
||||
|
||||
describe('Setting form fields', () => {
|
||||
test('BooleanField renders the expected content', async () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<Formik
|
||||
initialValues={{
|
||||
boolean: true,
|
||||
}}
|
||||
>
|
||||
{() => (
|
||||
<BooleanField
|
||||
name="boolean"
|
||||
config={{
|
||||
label: 'test',
|
||||
help_text: 'test',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Formik>
|
||||
const outerNode = document.createElement('div');
|
||||
document.body.appendChild(outerNode);
|
||||
const wrapper = mount(
|
||||
<I18nProvider>
|
||||
<Formik
|
||||
initialValues={{
|
||||
boolean: true,
|
||||
}}
|
||||
>
|
||||
{() => (
|
||||
<BooleanField
|
||||
name="boolean"
|
||||
config={{
|
||||
label: 'test',
|
||||
help_text: 'test',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Formik>
|
||||
</I18nProvider>,
|
||||
{
|
||||
attachTo: outerNode,
|
||||
}
|
||||
);
|
||||
expect(wrapper.find('Switch')).toHaveLength(1);
|
||||
expect(wrapper.find('Switch').prop('isChecked')).toBe(true);
|
||||
expect(wrapper.find('Switch').prop('isDisabled')).toBe(false);
|
||||
await act(async () => {
|
||||
wrapper.find('Switch').invoke('onChange')(false);
|
||||
wrapper
|
||||
.find('Switch label')
|
||||
.instance()
|
||||
.dispatchEvent(new Event('click'));
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Switch').prop('isChecked')).toBe(false);
|
||||
|
||||
@@ -1 +1 @@
|
||||
17.0.0
|
||||
17.0.1
|
||||
|
||||
@@ -39,7 +39,7 @@ The third part of the format is `<organization.name>`, which indicates that fiel
|
||||
|
||||
In the case where `organization` does not exist in the `related` field of label object detail, we append empty string `''` instead, which essentially does not alter the current identifier. So `Foo++` becomes final unique identifier and thus generate named URL to be `/api/v2/labels/Foo++/`.
|
||||
|
||||
An important aspect of generating unique identifiers for named URL is dealing with reserved characters. Because the identifier is part of a URL, the following reserved characters by URL standard should be escaped to its percentage encoding: `;/?:@=&[]`. For example, if an organization is named `;/?:@=&[]`, its unique identifier should be `%3B%2F%3F%3A%40%3D%26%5B%5D`. Another special reserved character is `+`, which is not reserved by URL standard but used by named URL to link different parts of an identifier. It is escaped by `[+]`. For example, if an organization is named `[+]`, tis unique identifier is `%5B[+]%5D`, where original `[` and `]` are percent encoded and `+` is converted to `[+]`.
|
||||
An important aspect of generating unique identifiers for named URL is dealing with reserved characters. Because the identifier is part of a URL, the following reserved characters by URL standard should be escaped to its percentage encoding: `;/?:@=&[]`. For example, if an organization is named `;/?:@=&[]`, its unique identifier should be `%3B%2F%3F%3A%40%3D%26%5B%5D`. Another special reserved character is `+`, which is not reserved by URL standard but used by named URL to link different parts of an identifier. It is escaped by `[+]`. For example, if an organization is named `[+]`, its unique identifier is `%5B[+]%5D`, where original `[` and `]` are percent encoded and `+` is converted to `[+]`.
|
||||
|
||||
`NAMED_URL_FORMATS` exclusively lists every resource that can have named URL; any resource not listed there has no named URL. `NAMED_URL_FORMATS` alone should be instructive enough for users to compose human-readable unique identifier and named URL themselves. For more convenience, every object of a resource that can have named URL will have a related field `named_url` that displays that object's named URL. Users can simply copy-paste that field for their custom usages. Also, users are expected to see indications in the help text of the API browser if a resource object has named URL.
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ dockerhub_base=ansible
|
||||
# Common Docker parameters
|
||||
awx_task_hostname=awx
|
||||
awx_web_hostname=awxweb
|
||||
# Local directory that is mounted in the awx_postgres docker container to place the db in
|
||||
postgres_data_dir="~/.awx/pgdocker"
|
||||
host_port=80
|
||||
host_port_ssl=443
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
---
|
||||
|
||||
- name: Create {{ postgres_data_dir }} directory
|
||||
file:
|
||||
path: "{{ postgres_data_dir }}"
|
||||
state: directory
|
||||
|
||||
- name: Get full path of postgres data dir
|
||||
shell: "echo {{ postgres_data_dir }}"
|
||||
register: fq_postgres_data_dir
|
||||
|
||||
Reference in New Issue
Block a user