diff --git a/.github/triage_replies.md b/.github/triage_replies.md index 6df2046a2f..f4cd6e9c9b 100644 --- a/.github/triage_replies.md +++ b/.github/triage_replies.md @@ -106,6 +106,13 @@ The Ansible Community is looking at building an EE that corresponds to all of th ### Oracle AWX We'd be happy to help if you can reproduce this with AWX since we do not have Oracle's Linux Automation Manager. If you need help with this specific version of Oracles Linux Automation Manager you will need to contact your Oracle for support. +### Community Resolved +Hi, + +We are happy to see that it appears a fix has been provided for your issue, so we will go ahead and close this ticket. Please feel free to reopen if any other problems arise. + + thanks so much for taking the time to write a thoughtful and helpful response to this issue! + ### AWX Release Subject: Announcing AWX Xa.Ya.za and AWX-Operator Xb.Yb.zb diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index 820494d303..6bd36f0102 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -38,9 +38,13 @@ jobs: - name: Build collection and publish to galaxy run: | COLLECTION_TEMPLATE_VERSION=true COLLECTION_NAMESPACE=${{ env.collection_namespace }} make build_collection - ansible-galaxy collection publish \ - --token=${{ secrets.GALAXY_TOKEN }} \ - awx_collection_build/${{ env.collection_namespace }}-awx-${{ github.event.release.tag_name }}.tar.gz + if [ "$(curl --head -sw '%{http_code}' https://galaxy.ansible.com/download/${{ env.collection_namespace }}-awx-${{ github.event.release.tag_name }}.tar.gz | tail -1)" == "302" ] ; then \ + echo "Galaxy release already done"; \ + else \ + ansible-galaxy collection publish \ + --token=${{ secrets.GALAXY_TOKEN }} \ + awx_collection_build/${{ env.collection_namespace }}-awx-${{ github.event.release.tag_name }}.tar.gz; \ + fi - name: Set official pypi info run: echo pypi_repo=pypi >> $GITHUB_ENV @@ -52,6 +56,7 @@ jobs: - name: Build awxkit and upload to pypi run: | + git reset --hard cd awxkit && python3 setup.py bdist_wheel twine upload \ -r ${{ env.pypi_repo }} \ @@ -74,4 +79,6 @@ jobs: docker tag ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} quay.io/${{ github.repository }}:latest docker push quay.io/${{ github.repository }}:${{ github.event.release.tag_name }} docker push quay.io/${{ github.repository }}:latest - + docker pull ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }} + docker tag ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }} quay.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }} + docker push quay.io/${{ github.repository_owner }}/awx-ee:${{ github.event.release.tag_name }} diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml index 042b6b7b0d..306fe3834c 100644 --- a/.github/workflows/stage.yml +++ b/.github/workflows/stage.yml @@ -84,6 +84,20 @@ jobs: -e push=yes \ -e awx_official=yes + - 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: tag awx-ee:latest with version input + run: | + docker pull quay.io/ansible/awx-ee:latest + docker tag quay.io/ansible/awx-ee:latest ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }} + docker push ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }} + - name: Build and stage awx-operator working-directory: awx-operator run: | @@ -103,6 +117,7 @@ jobs: env: AWX_TEST_IMAGE: ${{ github.repository }} AWX_TEST_VERSION: ${{ github.event.inputs.version }} + AWX_EE_TEST_IMAGE: ghcr.io/${{ github.repository_owner }}/awx-ee:${{ github.event.inputs.version }} - name: Create draft release for AWX working-directory: awx diff --git a/awx/api/conf.py b/awx/api/conf.py index fd1467cdde..b9c2ee701a 100644 --- a/awx/api/conf.py +++ b/awx/api/conf.py @@ -96,6 +96,15 @@ register( category=_('Authentication'), category_slug='authentication', ) +register( + 'ALLOW_METRICS_FOR_ANONYMOUS_USERS', + field_class=fields.BooleanField, + default=False, + label=_('Allow anonymous users to poll metrics'), + help_text=_('If true, anonymous users are allowed to poll metrics.'), + category=_('Authentication'), + category_slug='authentication', +) def authentication_validate(serializer, attrs): diff --git a/awx/api/views/metrics.py b/awx/api/views/metrics.py index 1634293cab..4c05819f13 100644 --- a/awx/api/views/metrics.py +++ b/awx/api/views/metrics.py @@ -5,9 +5,11 @@ import logging # Django +from django.conf import settings from django.utils.translation import gettext_lazy as _ # Django REST Framework +from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.exceptions import PermissionDenied @@ -31,9 +33,14 @@ class MetricsView(APIView): renderer_classes = [renderers.PlainTextRenderer, renderers.PrometheusJSONRenderer, renderers.BrowsableAPIRenderer] + def initialize_request(self, request, *args, **kwargs): + if settings.ALLOW_METRICS_FOR_ANONYMOUS_USERS: + self.permission_classes = (AllowAny,) + return super(APIView, self).initialize_request(request, *args, **kwargs) + def get(self, request): '''Show Metrics Details''' - if request.user.is_superuser or request.user.is_system_auditor: + if settings.ALLOW_METRICS_FOR_ANONYMOUS_USERS or request.user.is_superuser or request.user.is_system_auditor: metrics_to_show = '' if not request.query_params.get('subsystemonly', "0") == "1": metrics_to_show += metrics().decode('UTF-8') diff --git a/awx/main/credential_plugins/hashivault.py b/awx/main/credential_plugins/hashivault.py index 1a636bdbf9..0a2b9171b9 100644 --- a/awx/main/credential_plugins/hashivault.py +++ b/awx/main/credential_plugins/hashivault.py @@ -1,6 +1,7 @@ import copy import os import pathlib +import time from urllib.parse import urljoin from .plugin import CredentialPlugin, CertFiles, raise_for_status @@ -247,7 +248,15 @@ def kv_backend(**kwargs): request_url = urljoin(url, '/'.join(['v1'] + path_segments)).rstrip('/') with CertFiles(cacert) as cert: request_kwargs['verify'] = cert - response = sess.get(request_url, **request_kwargs) + request_retries = 0 + while request_retries < 5: + response = sess.get(request_url, **request_kwargs) + # https://developer.hashicorp.com/vault/docs/enterprise/consistency + if response.status_code == 412: + request_retries += 1 + time.sleep(1) + else: + break raise_for_status(response) json = response.json() @@ -289,8 +298,15 @@ def ssh_backend(**kwargs): with CertFiles(cacert) as cert: request_kwargs['verify'] = cert - resp = sess.post(request_url, **request_kwargs) - + request_retries = 0 + while request_retries < 5: + resp = sess.post(request_url, **request_kwargs) + # https://developer.hashicorp.com/vault/docs/enterprise/consistency + if resp.status_code == 412: + request_retries += 1 + time.sleep(1) + else: + break raise_for_status(resp) return resp.json()['data']['signed_key'] diff --git a/awx/main/dispatch/worker/callback.py b/awx/main/dispatch/worker/callback.py index 0578a4ff97..b0588265a4 100644 --- a/awx/main/dispatch/worker/callback.py +++ b/awx/main/dispatch/worker/callback.py @@ -3,14 +3,12 @@ import logging import os import signal import time -import traceback import datetime from django.conf import settings from django.utils.functional import cached_property from django.utils.timezone import now as tz_now -from django.db import DatabaseError, OperationalError, transaction, connection as django_connection -from django.db.utils import InterfaceError, InternalError +from django.db import transaction, connection as django_connection from django_guid import set_guid import psutil @@ -64,6 +62,7 @@ class CallbackBrokerWorker(BaseWorker): """ MAX_RETRIES = 2 + INDIVIDUAL_EVENT_RETRIES = 3 last_stats = time.time() last_flush = time.time() total = 0 @@ -164,38 +163,48 @@ class CallbackBrokerWorker(BaseWorker): else: # only calculate the seconds if the created time already has been set metrics_total_job_event_processing_seconds += e.modified - e.created metrics_duration_to_save = time.perf_counter() + saved_events = [] try: cls.objects.bulk_create(events) metrics_bulk_events_saved += len(events) + saved_events = events + self.buff[cls] = [] except Exception as exc: - logger.warning(f'Error in events bulk_create, will try indiviually up to 5 errors, error {str(exc)}') + # If the database is flaking, let ensure_connection throw a general exception + # will be caught by the outer loop, which goes into a proper sleep and retry loop + django_connection.ensure_connection() + logger.warning(f'Error in events bulk_create, will try indiviually, error: {str(exc)}') # if an exception occurs, we should re-attempt to save the # events one-by-one, because something in the list is # broken/stale - consecutive_errors = 0 - events_saved = 0 metrics_events_batch_save_errors += 1 - for e in events: + for e in events.copy(): try: e.save() - events_saved += 1 - consecutive_errors = 0 + metrics_singular_events_saved += 1 + events.remove(e) + saved_events.append(e) # Importantly, remove successfully saved events from the buffer except Exception as exc_indv: - consecutive_errors += 1 - logger.info(f'Database Error Saving individual Job Event, error {str(exc_indv)}') - if consecutive_errors >= 5: - raise - metrics_singular_events_saved += events_saved - if events_saved == 0: - raise + retry_count = getattr(e, '_retry_count', 0) + 1 + e._retry_count = retry_count + + # special sanitization logic for postgres treatment of NUL 0x00 char + if (retry_count == 1) and isinstance(exc_indv, ValueError) and ("\x00" in e.stdout): + e.stdout = e.stdout.replace("\x00", "") + + if retry_count >= self.INDIVIDUAL_EVENT_RETRIES: + logger.error(f'Hit max retries ({retry_count}) saving individual Event error: {str(exc_indv)}\ndata:\n{e.__dict__}') + events.remove(e) + else: + logger.info(f'Database Error Saving individual Event uuid={e.uuid} try={retry_count}, error: {str(exc_indv)}') + metrics_duration_to_save = time.perf_counter() - metrics_duration_to_save - for e in events: + for e in saved_events: if not getattr(e, '_skip_websocket_message', False): metrics_events_broadcast += 1 emit_event_detail(e) if getattr(e, '_notification_trigger_event', False): job_stats_wrapup(getattr(e, e.JOB_REFERENCE), event=e) - self.buff = {} self.last_flush = time.time() # only update metrics if we saved events if (metrics_bulk_events_saved + metrics_singular_events_saved) > 0: @@ -267,20 +276,16 @@ class CallbackBrokerWorker(BaseWorker): try: self.flush(force=flush) break - except (OperationalError, InterfaceError, InternalError) as exc: + except Exception as exc: + # Aside form bugs, exceptions here are assumed to be due to database flake if retries >= self.MAX_RETRIES: logger.exception('Worker could not re-establish database connectivity, giving up on one or more events.') + self.buff = {} return delay = 60 * retries logger.warning(f'Database Error Flushing Job Events, retry #{retries + 1} in {delay} seconds: {str(exc)}') django_connection.close() time.sleep(delay) retries += 1 - except DatabaseError: - logger.exception('Database Error Flushing Job Events') - django_connection.close() - break - except Exception as exc: - tb = traceback.format_exc() - logger.error('Callback Task Processor Raised Exception: %r', exc) - logger.error('Detail: {}'.format(tb)) + except Exception: + logger.exception(f'Callback Task Processor Raised Unexpected Exception processing event data:\n{body}') diff --git a/awx/main/tests/functional/commands/test_callback_receiver.py b/awx/main/tests/functional/commands/test_callback_receiver.py index 389edf6ffb..234392fb44 100644 --- a/awx/main/tests/functional/commands/test_callback_receiver.py +++ b/awx/main/tests/functional/commands/test_callback_receiver.py @@ -1,7 +1,15 @@ import pytest +import time +from unittest import mock +from uuid import uuid4 + +from django.test import TransactionTestCase + +from awx.main.dispatch.worker.callback import job_stats_wrapup, CallbackBrokerWorker -from awx.main.dispatch.worker.callback import job_stats_wrapup from awx.main.models.jobs import Job +from awx.main.models.inventory import InventoryUpdate, InventorySource +from awx.main.models.events import InventoryUpdateEvent @pytest.mark.django_db @@ -24,3 +32,108 @@ def test_wrapup_does_send_notifications(mocker): job.refresh_from_db() assert job.host_status_counts == {} mock.assert_called_once_with('succeeded') + + +class FakeRedis: + def keys(self, *args, **kwargs): + return [] + + def set(self): + pass + + def get(self): + return None + + @classmethod + def from_url(cls, *args, **kwargs): + return cls() + + def pipeline(self): + return self + + +class TestCallbackBrokerWorker(TransactionTestCase): + @pytest.fixture(autouse=True) + def turn_off_websockets(self): + with mock.patch('awx.main.dispatch.worker.callback.emit_event_detail', lambda *a, **kw: None): + yield + + def get_worker(self): + with mock.patch('redis.Redis', new=FakeRedis): # turn off redis stuff + return CallbackBrokerWorker() + + def event_create_kwargs(self): + inventory_update = InventoryUpdate.objects.create(source='file', inventory_source=InventorySource.objects.create(source='file')) + return dict(inventory_update=inventory_update, created=inventory_update.created) + + def test_flush_with_valid_event(self): + worker = self.get_worker() + events = [InventoryUpdateEvent(uuid=str(uuid4()), **self.event_create_kwargs())] + worker.buff = {InventoryUpdateEvent: events} + worker.flush() + assert worker.buff.get(InventoryUpdateEvent, []) == [] + assert InventoryUpdateEvent.objects.filter(uuid=events[0].uuid).count() == 1 + + def test_flush_with_invalid_event(self): + worker = self.get_worker() + kwargs = self.event_create_kwargs() + events = [ + InventoryUpdateEvent(uuid=str(uuid4()), stdout='good1', **kwargs), + InventoryUpdateEvent(uuid=str(uuid4()), stdout='bad', counter=-2, **kwargs), + InventoryUpdateEvent(uuid=str(uuid4()), stdout='good2', **kwargs), + ] + worker.buff = {InventoryUpdateEvent: events.copy()} + worker.flush() + assert InventoryUpdateEvent.objects.filter(uuid=events[0].uuid).count() == 1 + assert InventoryUpdateEvent.objects.filter(uuid=events[1].uuid).count() == 0 + assert InventoryUpdateEvent.objects.filter(uuid=events[2].uuid).count() == 1 + assert worker.buff == {InventoryUpdateEvent: [events[1]]} + + def test_duplicate_key_not_saved_twice(self): + worker = self.get_worker() + events = [InventoryUpdateEvent(uuid=str(uuid4()), **self.event_create_kwargs())] + worker.buff = {InventoryUpdateEvent: events.copy()} + worker.flush() + + # put current saved event in buffer (error case) + worker.buff = {InventoryUpdateEvent: [InventoryUpdateEvent.objects.get(uuid=events[0].uuid)]} + worker.last_flush = time.time() - 2.0 + # here, the bulk_create will fail with UNIQUE constraint violation, but individual saves should resolve it + worker.flush() + assert InventoryUpdateEvent.objects.filter(uuid=events[0].uuid).count() == 1 + assert worker.buff.get(InventoryUpdateEvent, []) == [] + + def test_give_up_on_bad_event(self): + worker = self.get_worker() + events = [InventoryUpdateEvent(uuid=str(uuid4()), counter=-2, **self.event_create_kwargs())] + worker.buff = {InventoryUpdateEvent: events.copy()} + + for i in range(5): + worker.last_flush = time.time() - 2.0 + worker.flush() + + # Could not save, should be logged, and buffer should be cleared + assert worker.buff.get(InventoryUpdateEvent, []) == [] + assert InventoryUpdateEvent.objects.filter(uuid=events[0].uuid).count() == 0 # sanity + + def test_postgres_invalid_NUL_char(self): + # In postgres, text fields reject NUL character, 0x00 + # tests use sqlite3 which will not raise an error + # but we can still test that it is sanitized before saving + worker = self.get_worker() + kwargs = self.event_create_kwargs() + events = [InventoryUpdateEvent(uuid=str(uuid4()), stdout="\x00", **kwargs)] + assert "\x00" in events[0].stdout # sanity + worker.buff = {InventoryUpdateEvent: events.copy()} + + with mock.patch.object(InventoryUpdateEvent.objects, 'bulk_create', side_effect=ValueError): + with mock.patch.object(events[0], 'save', side_effect=ValueError): + worker.flush() + + assert "\x00" not in events[0].stdout + + worker.last_flush = time.time() - 2.0 + worker.flush() + + event = InventoryUpdateEvent.objects.get(uuid=events[0].uuid) + assert "\x00" not in event.stdout diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 5488f50412..e0d95adb5d 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -418,6 +418,9 @@ AUTH_BASIC_ENABLED = True # when trying to access a UI page that requries authentication. LOGIN_REDIRECT_OVERRIDE = '' +# Note: This setting may be overridden by database settings. +ALLOW_METRICS_FOR_ANONYMOUS_USERS = False + DEVSERVER_DEFAULT_ADDR = '0.0.0.0' DEVSERVER_DEFAULT_PORT = '8013' diff --git a/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js b/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js index f13208e4f0..69ed14eb93 100644 --- a/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js +++ b/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.js @@ -34,8 +34,14 @@ const QS_CONFIG = getQSConfig('template', { order_by: 'name', }); -function RelatedTemplateList({ searchParams, projectName = null }) { - const { id: projectId } = useParams(); +const resources = { + projects: 'project', + inventories: 'inventory', + credentials: 'credentials', +}; + +function RelatedTemplateList({ searchParams, resourceName = null }) { + const { id } = useParams(); const location = useLocation(); const { addToast, Toast, toastProps } = useToast(); @@ -129,12 +135,19 @@ function RelatedTemplateList({ searchParams, projectName = null }) { actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); let linkTo = ''; - - if (projectName) { - const qs = encodeQueryString({ - project_id: projectId, - project_name: projectName, - }); + if (resourceName) { + const queryString = { + resource_id: id, + resource_name: resourceName, + resource_type: resources[location.pathname.split('/')[1]], + resource_kind: null, + }; + if (Array.isArray(resourceName)) { + const [name, kind] = resourceName; + queryString.resource_name = name; + queryString.resource_kind = kind; + } + const qs = encodeQueryString(queryString); linkTo = `/templates/job_template/add/?${qs}`; } else { linkTo = '/templates/job_template/add'; diff --git a/awx/ui/src/components/RelatedTemplateList/relatedTemplateHelpers.js b/awx/ui/src/components/RelatedTemplateList/relatedTemplateHelpers.js new file mode 100644 index 0000000000..95ecb0ce85 --- /dev/null +++ b/awx/ui/src/components/RelatedTemplateList/relatedTemplateHelpers.js @@ -0,0 +1 @@ +/* eslint-disable import/prefer-default-export */ diff --git a/awx/ui/src/screens/Credential/Credential.js b/awx/ui/src/screens/Credential/Credential.js index 45c507e23e..ce0509146d 100644 --- a/awx/ui/src/screens/Credential/Credential.js +++ b/awx/ui/src/screens/Credential/Credential.js @@ -22,6 +22,16 @@ import { CredentialsAPI } from 'api'; import CredentialDetail from './CredentialDetail'; import CredentialEdit from './CredentialEdit'; +const jobTemplateCredentialTypes = [ + 'machine', + 'cloud', + 'net', + 'ssh', + 'vault', + 'kubernetes', + 'cryptography', +]; + function Credential({ setBreadcrumb }) { const { pathname } = useLocation(); @@ -75,13 +85,14 @@ function Credential({ setBreadcrumb }) { link: `/credentials/${id}/access`, id: 1, }, - { + ]; + if (jobTemplateCredentialTypes.includes(credential?.kind)) { + tabsArray.push({ name: t`Job Templates`, link: `/credentials/${id}/job_templates`, id: 2, - }, - ]; - + }); + } let showCardHeader = true; if (pathname.endsWith('edit') || pathname.endsWith('add')) { @@ -133,6 +144,7 @@ function Credential({ setBreadcrumb }) { , diff --git a/awx/ui/src/screens/Credential/Credential.test.js b/awx/ui/src/screens/Credential/Credential.test.js index b66619c877..2df3162205 100644 --- a/awx/ui/src/screens/Credential/Credential.test.js +++ b/awx/ui/src/screens/Credential/Credential.test.js @@ -6,7 +6,8 @@ import { mountWithContexts, waitForElement, } from '../../../testUtils/enzymeHelpers'; -import mockCredential from './shared/data.scmCredential.json'; +import mockMachineCredential from './shared/data.machineCredential.json'; +import mockSCMCredential from './shared/data.scmCredential.json'; import Credential from './Credential'; jest.mock('../../api'); @@ -21,13 +22,10 @@ jest.mock('react-router-dom', () => ({ describe('', () => { let wrapper; - beforeEach(() => { + test('initially renders user-based machine credential successfully', async () => { CredentialsAPI.readDetail.mockResolvedValueOnce({ - data: mockCredential, + data: mockMachineCredential, }); - }); - - test('initially renders user-based credential successfully', async () => { await act(async () => { wrapper = mountWithContexts( {}} />); }); @@ -36,6 +34,18 @@ describe('', () => { expect(wrapper.find('RoutedTabs li').length).toBe(4); }); + test('initially renders user-based SCM credential successfully', async () => { + CredentialsAPI.readDetail.mockResolvedValueOnce({ + data: mockSCMCredential, + }); + await act(async () => { + wrapper = mountWithContexts( {}} />); + }); + wrapper.update(); + expect(wrapper.find('Credential').length).toBe(1); + expect(wrapper.find('RoutedTabs li').length).toBe(3); + }); + test('should render expected tabs', async () => { const expectedTabs = [ 'Back to Credentials', diff --git a/awx/ui/src/screens/Inventory/Inventory.js b/awx/ui/src/screens/Inventory/Inventory.js index d26c0fc5ce..53da122cd6 100644 --- a/awx/ui/src/screens/Inventory/Inventory.js +++ b/awx/ui/src/screens/Inventory/Inventory.js @@ -181,6 +181,7 @@ function Inventory({ setBreadcrumb }) { > , diff --git a/awx/ui/src/screens/Project/Project.js b/awx/ui/src/screens/Project/Project.js index f3500d3eda..6ec4bd9558 100644 --- a/awx/ui/src/screens/Project/Project.js +++ b/awx/ui/src/screens/Project/Project.js @@ -179,7 +179,7 @@ function Project({ setBreadcrumb }) { searchParams={{ project__id: project.id, }} - projectName={project.name} + resourceName={project.name} /> {project?.scm_type && project.scm_type !== '' && ( diff --git a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.js b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.js index f4d0f4f49a..ebb171bb1e 100644 --- a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.js +++ b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.js @@ -9,29 +9,31 @@ function JobTemplateAdd() { const [formSubmitError, setFormSubmitError] = useState(null); const history = useHistory(); - const projectParams = { - project_id: null, - project_name: null, + const resourceParams = { + resource_id: null, + resource_name: null, + resource_type: null, + resource_kind: null, }; history.location.search .replace(/^\?/, '') .split('&') .map((s) => s.split('=')) .forEach(([key, val]) => { - if (!(key in projectParams)) { + if (!(key in resourceParams)) { return; } - projectParams[key] = decodeURIComponent(val); + resourceParams[key] = decodeURIComponent(val); }); - let projectValues = null; + let resourceValues = null; - if ( - Object.values(projectParams).filter((item) => item !== null).length === 2 - ) { - projectValues = { - id: projectParams.project_id, - name: projectParams.project_name, + if (history.location.search.includes('resource_id' && 'resource_name')) { + resourceValues = { + id: resourceParams.resource_id, + name: resourceParams.resource_name, + type: resourceParams.resource_type, + kind: resourceParams.resource_kind, // refers to credential kind }; } @@ -122,7 +124,7 @@ function JobTemplateAdd() { handleCancel={handleCancel} handleSubmit={handleSubmit} submitError={formSubmitError} - projectValues={projectValues} + resourceValues={resourceValues} isOverrideDisabledLookup /> diff --git a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js index 3fd63394dc..e50b91d8c8 100644 --- a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js +++ b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js @@ -274,9 +274,14 @@ describe('', () => { test('should parse and pre-fill project field from query params', async () => { const history = createMemoryHistory({ initialEntries: [ - '/templates/job_template/add/add?project_id=6&project_name=Demo%20Project', + '/templates/job_template/add?resource_id=6&resource_name=Demo%20Project&resource_type=project', ], }); + ProjectsAPI.read.mockResolvedValueOnce({ + count: 1, + results: [{ name: 'foo', id: 1, allow_override: true, organization: 1 }], + }); + ProjectsAPI.readOptions.mockResolvedValueOnce({}); let wrapper; await act(async () => { wrapper = mountWithContexts(, { @@ -284,8 +289,9 @@ describe('', () => { }); }); await waitForElement(wrapper, 'EmptyStateBody', (el) => el.length === 0); + expect(wrapper.find('input#project').prop('value')).toEqual('Demo Project'); - expect(ProjectsAPI.readPlaybooks).toBeCalledWith('6'); + expect(ProjectsAPI.readPlaybooks).toBeCalledWith(6); }); test('should not call ProjectsAPI.readPlaybooks if there is no project', async () => { diff --git a/awx/ui/src/screens/Template/shared/JobTemplateForm.js b/awx/ui/src/screens/Template/shared/JobTemplateForm.js index 7621601e9e..dcdfd0d956 100644 --- a/awx/ui/src/screens/Template/shared/JobTemplateForm.js +++ b/awx/ui/src/screens/Template/shared/JobTemplateForm.js @@ -690,7 +690,7 @@ JobTemplateForm.defaultProps = { }; const FormikApp = withFormik({ - mapPropsToValues({ projectValues = {}, template = {} }) { + mapPropsToValues({ resourceValues = null, template = {} }) { const { summary_fields = { labels: { results: [] }, @@ -698,7 +698,7 @@ const FormikApp = withFormik({ }, } = template; - return { + const initialValues = { allow_callbacks: template.allow_callbacks || false, allow_simultaneous: template.allow_simultaneous || false, ask_credential_on_launch: template.ask_credential_on_launch || false, @@ -739,7 +739,7 @@ const FormikApp = withFormik({ playbook: template.playbook || '', prevent_instance_group_fallback: template.prevent_instance_group_fallback || false, - project: summary_fields?.project || projectValues || null, + project: summary_fields?.project || null, scm_branch: template.scm_branch || '', skip_tags: template.skip_tags || '', timeout: template.timeout || 0, @@ -756,6 +756,24 @@ const FormikApp = withFormik({ execution_environment: template.summary_fields?.execution_environment || null, }; + if (resourceValues !== null) { + if (resourceValues.type === 'credentials') { + initialValues[resourceValues.type] = [ + { + id: parseInt(resourceValues.id, 10), + name: resourceValues.name, + kind: resourceValues.kind, + }, + ]; + } else { + initialValues[resourceValues.type] = { + id: parseInt(resourceValues.id, 10), + name: resourceValues.name, + }; + } + } + + return initialValues; }, handleSubmit: async (values, { props, setErrors }) => { try { diff --git a/awx_collection/meta/runtime.yml b/awx_collection/meta/runtime.yml index b23d5b87e2..e59becd4ea 100644 --- a/awx_collection/meta/runtime.yml +++ b/awx_collection/meta/runtime.yml @@ -46,90 +46,216 @@ action_groups: plugin_routing: inventory: tower: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* plugins have been deprecated, use awx.awx.controller instead. redirect: awx.awx.controller lookup: tower_api: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* plugins have been deprecated, use awx.awx.controller_api instead. redirect: awx.awx.controller_api tower_schedule_rrule: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* plugins have been deprecated, use awx.awx.schedule_rrule instead. redirect: awx.awx.schedule_rrule modules: tower_ad_hoc_command_cancel: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.ad_hoc_command_cancel instead. redirect: awx.awx.ad_hoc_command_cancel tower_ad_hoc_command_wait: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.ad_hoc_command_wait instead. redirect: awx.awx.ad_hoc_command_wait tower_ad_hoc_command: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.ad_hoc_command instead. redirect: awx.awx.ad_hoc_command tower_application: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.application instead. redirect: awx.awx.application tower_meta: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.controller_meta instead. redirect: awx.awx.controller_meta tower_credential_input_source: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.credential_input_source instead. redirect: awx.awx.credential_input_source tower_credential_type: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.credential_type instead. redirect: awx.awx.credential_type tower_credential: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.credential instead. redirect: awx.awx.credential tower_execution_environment: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.execution_environment instead. redirect: awx.awx.execution_environment tower_export: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.export instead. redirect: awx.awx.export tower_group: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.group instead. redirect: awx.awx.group tower_host: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.host instead. redirect: awx.awx.host tower_import: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.import instead. redirect: awx.awx.import tower_instance_group: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.instance_group instead. redirect: awx.awx.instance_group tower_inventory_source_update: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.inventory_source_update instead. redirect: awx.awx.inventory_source_update tower_inventory_source: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.inventory_source instead. redirect: awx.awx.inventory_source tower_inventory: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.inventory instead. redirect: awx.awx.inventory tower_job_cancel: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.job_cancel instead. redirect: awx.awx.job_cancel tower_job_launch: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.job_launch instead. redirect: awx.awx.job_launch tower_job_list: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.job_list instead. redirect: awx.awx.job_list tower_job_template: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.job_template instead. redirect: awx.awx.job_template tower_job_wait: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.job_wait instead. redirect: awx.awx.job_wait tower_label: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.label instead. redirect: awx.awx.label tower_license: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.license instead. redirect: awx.awx.license tower_notification_template: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.notification_template instead. redirect: awx.awx.notification_template tower_notification: redirect: awx.awx.notification_template tower_organization: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.organization instead. redirect: awx.awx.organization tower_project_update: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.project_update instead. redirect: awx.awx.project_update tower_project: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.project instead. redirect: awx.awx.project tower_role: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.role instead. redirect: awx.awx.role tower_schedule: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.schedule instead. redirect: awx.awx.schedule tower_settings: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.settings instead. redirect: awx.awx.settings tower_team: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.team instead. redirect: awx.awx.team tower_token: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.token instead. redirect: awx.awx.token tower_user: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.user instead. redirect: awx.awx.user tower_workflow_approval: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_approval instead. redirect: awx.awx.workflow_approval tower_workflow_job_template_node: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_job_template_node instead. redirect: awx.awx.workflow_job_template_node tower_workflow_job_template: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_job_template instead. redirect: awx.awx.workflow_job_template tower_workflow_launch: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_launch instead. redirect: awx.awx.workflow_launch tower_workflow_node_wait: + deprecation: + removal_date: '2022-01-23' + warning_text: The tower_* modules have been deprecated, use awx.awx.workflow_node_wait instead. redirect: awx.awx.workflow_node_wait diff --git a/awx_collection/plugins/modules/group.py b/awx_collection/plugins/modules/group.py index 973fb7744d..c91bf164d9 100644 --- a/awx_collection/plugins/modules/group.py +++ b/awx_collection/plugins/modules/group.py @@ -128,7 +128,7 @@ def main(): description = module.params.get('description') state = module.params.pop('state') preserve_existing_hosts = module.params.get('preserve_existing_hosts') - preserve_existing_children = module.params.get('preserve_existing_groups') + preserve_existing_children = module.params.get('preserve_existing_children') variables = module.params.get('variables') # Attempt to look up the related items the user specified (these will fail the module if not found) diff --git a/awx_collection/plugins/modules/tower_ad_hoc_command.py b/awx_collection/plugins/modules/tower_ad_hoc_command.py deleted file mode 120000 index 1b02428042..0000000000 --- a/awx_collection/plugins/modules/tower_ad_hoc_command.py +++ /dev/null @@ -1 +0,0 @@ -ad_hoc_command.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_ad_hoc_command_cancel.py b/awx_collection/plugins/modules/tower_ad_hoc_command_cancel.py deleted file mode 120000 index 1d9c64563b..0000000000 --- a/awx_collection/plugins/modules/tower_ad_hoc_command_cancel.py +++ /dev/null @@ -1 +0,0 @@ -ad_hoc_command_cancel.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_ad_hoc_command_wait.py b/awx_collection/plugins/modules/tower_ad_hoc_command_wait.py deleted file mode 120000 index 50cc9f6eab..0000000000 --- a/awx_collection/plugins/modules/tower_ad_hoc_command_wait.py +++ /dev/null @@ -1 +0,0 @@ -ad_hoc_command_wait.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_application.py b/awx_collection/plugins/modules/tower_application.py deleted file mode 120000 index cc28a46af5..0000000000 --- a/awx_collection/plugins/modules/tower_application.py +++ /dev/null @@ -1 +0,0 @@ -application.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_controller_meta.py b/awx_collection/plugins/modules/tower_controller_meta.py deleted file mode 120000 index 603f9fa251..0000000000 --- a/awx_collection/plugins/modules/tower_controller_meta.py +++ /dev/null @@ -1 +0,0 @@ -controller_meta.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_credential.py b/awx_collection/plugins/modules/tower_credential.py deleted file mode 120000 index 76fc468892..0000000000 --- a/awx_collection/plugins/modules/tower_credential.py +++ /dev/null @@ -1 +0,0 @@ -credential.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_credential_input_source.py b/awx_collection/plugins/modules/tower_credential_input_source.py deleted file mode 120000 index b6824f7983..0000000000 --- a/awx_collection/plugins/modules/tower_credential_input_source.py +++ /dev/null @@ -1 +0,0 @@ -credential_input_source.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_credential_type.py b/awx_collection/plugins/modules/tower_credential_type.py deleted file mode 120000 index 3ef2c5aaa1..0000000000 --- a/awx_collection/plugins/modules/tower_credential_type.py +++ /dev/null @@ -1 +0,0 @@ -credential_type.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_execution_environment.py b/awx_collection/plugins/modules/tower_execution_environment.py deleted file mode 120000 index 0436ddac1d..0000000000 --- a/awx_collection/plugins/modules/tower_execution_environment.py +++ /dev/null @@ -1 +0,0 @@ -execution_environment.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py deleted file mode 120000 index b9ead459dc..0000000000 --- a/awx_collection/plugins/modules/tower_export.py +++ /dev/null @@ -1 +0,0 @@ -export.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_group.py b/awx_collection/plugins/modules/tower_group.py deleted file mode 120000 index 0d50916a64..0000000000 --- a/awx_collection/plugins/modules/tower_group.py +++ /dev/null @@ -1 +0,0 @@ -group.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_host.py b/awx_collection/plugins/modules/tower_host.py deleted file mode 120000 index 36a0bc2c59..0000000000 --- a/awx_collection/plugins/modules/tower_host.py +++ /dev/null @@ -1 +0,0 @@ -host.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py deleted file mode 120000 index b0354fac74..0000000000 --- a/awx_collection/plugins/modules/tower_import.py +++ /dev/null @@ -1 +0,0 @@ -import.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_instance_group.py b/awx_collection/plugins/modules/tower_instance_group.py deleted file mode 120000 index f7f770d778..0000000000 --- a/awx_collection/plugins/modules/tower_instance_group.py +++ /dev/null @@ -1 +0,0 @@ -instance_group.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_inventory.py b/awx_collection/plugins/modules/tower_inventory.py deleted file mode 120000 index f3f0a4990c..0000000000 --- a/awx_collection/plugins/modules/tower_inventory.py +++ /dev/null @@ -1 +0,0 @@ -inventory.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py deleted file mode 120000 index 462d6066fd..0000000000 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ /dev/null @@ -1 +0,0 @@ -inventory_source.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_inventory_source_update.py b/awx_collection/plugins/modules/tower_inventory_source_update.py deleted file mode 120000 index a283dfccb1..0000000000 --- a/awx_collection/plugins/modules/tower_inventory_source_update.py +++ /dev/null @@ -1 +0,0 @@ -inventory_source_update.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_job_cancel.py b/awx_collection/plugins/modules/tower_job_cancel.py deleted file mode 120000 index 6298f026cb..0000000000 --- a/awx_collection/plugins/modules/tower_job_cancel.py +++ /dev/null @@ -1 +0,0 @@ -job_cancel.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_job_launch.py b/awx_collection/plugins/modules/tower_job_launch.py deleted file mode 120000 index dbcb7048bc..0000000000 --- a/awx_collection/plugins/modules/tower_job_launch.py +++ /dev/null @@ -1 +0,0 @@ -job_launch.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_job_list.py b/awx_collection/plugins/modules/tower_job_list.py deleted file mode 120000 index 45e0ea6fe4..0000000000 --- a/awx_collection/plugins/modules/tower_job_list.py +++ /dev/null @@ -1 +0,0 @@ -job_list.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_job_template.py b/awx_collection/plugins/modules/tower_job_template.py deleted file mode 120000 index 4561927af7..0000000000 --- a/awx_collection/plugins/modules/tower_job_template.py +++ /dev/null @@ -1 +0,0 @@ -job_template.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_job_wait.py b/awx_collection/plugins/modules/tower_job_wait.py deleted file mode 120000 index 488cb4683d..0000000000 --- a/awx_collection/plugins/modules/tower_job_wait.py +++ /dev/null @@ -1 +0,0 @@ -job_wait.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_label.py b/awx_collection/plugins/modules/tower_label.py deleted file mode 120000 index 593aec76b8..0000000000 --- a/awx_collection/plugins/modules/tower_label.py +++ /dev/null @@ -1 +0,0 @@ -label.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_license.py b/awx_collection/plugins/modules/tower_license.py deleted file mode 120000 index 467811cd59..0000000000 --- a/awx_collection/plugins/modules/tower_license.py +++ /dev/null @@ -1 +0,0 @@ -license.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_notification_template.py b/awx_collection/plugins/modules/tower_notification_template.py deleted file mode 120000 index f24d407167..0000000000 --- a/awx_collection/plugins/modules/tower_notification_template.py +++ /dev/null @@ -1 +0,0 @@ -notification_template.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_organization.py b/awx_collection/plugins/modules/tower_organization.py deleted file mode 120000 index 2da5304b4a..0000000000 --- a/awx_collection/plugins/modules/tower_organization.py +++ /dev/null @@ -1 +0,0 @@ -organization.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py deleted file mode 120000 index 41d1fa306a..0000000000 --- a/awx_collection/plugins/modules/tower_project.py +++ /dev/null @@ -1 +0,0 @@ -project.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_project_update.py b/awx_collection/plugins/modules/tower_project_update.py deleted file mode 120000 index 6f22e2a4b1..0000000000 --- a/awx_collection/plugins/modules/tower_project_update.py +++ /dev/null @@ -1 +0,0 @@ -project_update.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py deleted file mode 120000 index 0a520b759b..0000000000 --- a/awx_collection/plugins/modules/tower_role.py +++ /dev/null @@ -1 +0,0 @@ -role.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_schedule.py b/awx_collection/plugins/modules/tower_schedule.py deleted file mode 120000 index a21a88885c..0000000000 --- a/awx_collection/plugins/modules/tower_schedule.py +++ /dev/null @@ -1 +0,0 @@ -schedule.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_settings.py b/awx_collection/plugins/modules/tower_settings.py deleted file mode 120000 index fff7c2ed4a..0000000000 --- a/awx_collection/plugins/modules/tower_settings.py +++ /dev/null @@ -1 +0,0 @@ -settings.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_team.py b/awx_collection/plugins/modules/tower_team.py deleted file mode 120000 index 320689b4cd..0000000000 --- a/awx_collection/plugins/modules/tower_team.py +++ /dev/null @@ -1 +0,0 @@ -team.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py deleted file mode 120000 index 0c41c0d586..0000000000 --- a/awx_collection/plugins/modules/tower_token.py +++ /dev/null @@ -1 +0,0 @@ -token.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_user.py b/awx_collection/plugins/modules/tower_user.py deleted file mode 120000 index 576f943a25..0000000000 --- a/awx_collection/plugins/modules/tower_user.py +++ /dev/null @@ -1 +0,0 @@ -user.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_workflow_approval.py b/awx_collection/plugins/modules/tower_workflow_approval.py deleted file mode 120000 index 76ba8f3be2..0000000000 --- a/awx_collection/plugins/modules/tower_workflow_approval.py +++ /dev/null @@ -1 +0,0 @@ -workflow_approval.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_workflow_job_template.py b/awx_collection/plugins/modules/tower_workflow_job_template.py deleted file mode 120000 index 914891e32a..0000000000 --- a/awx_collection/plugins/modules/tower_workflow_job_template.py +++ /dev/null @@ -1 +0,0 @@ -workflow_job_template.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py deleted file mode 120000 index 406b3cec5b..0000000000 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ /dev/null @@ -1 +0,0 @@ -workflow_job_template_node.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_workflow_launch.py b/awx_collection/plugins/modules/tower_workflow_launch.py deleted file mode 120000 index d0a93529d8..0000000000 --- a/awx_collection/plugins/modules/tower_workflow_launch.py +++ /dev/null @@ -1 +0,0 @@ -workflow_launch.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/tower_workflow_node_wait.py b/awx_collection/plugins/modules/tower_workflow_node_wait.py deleted file mode 120000 index 25bf1d0a87..0000000000 --- a/awx_collection/plugins/modules/tower_workflow_node_wait.py +++ /dev/null @@ -1 +0,0 @@ -workflow_node_wait.py \ No newline at end of file diff --git a/awx_collection/plugins/modules/workflow_job_template.py b/awx_collection/plugins/modules/workflow_job_template.py index ba4d5cf102..41c5b66573 100644 --- a/awx_collection/plugins/modules/workflow_job_template.py +++ b/awx_collection/plugins/modules/workflow_job_template.py @@ -19,7 +19,6 @@ author: "John Westcott IV (@john-westcott-iv)" short_description: create, update, or destroy Automation Platform Controller workflow job templates. description: - Create, update, or destroy Automation Platform Controller workflow job templates. - - Replaces the deprecated tower_workflow_template module. - Use workflow_job_template_node after this, or use the workflow_nodes parameter to build the workflow's graph options: name: @@ -614,6 +613,10 @@ def create_workflow_nodes(module, response, workflow_nodes, workflow_id): if workflow_node['unified_job_template']['type'] != 'workflow_approval': module.fail_json(msg="Unable to Find unified_job_template: {0}".format(search_fields)) + inventory = workflow_node.get('inventory') + if inventory: + workflow_node_fields['inventory'] = module.resolve_name_to_id('inventories', inventory) + # Lookup Values for other fields for field_name in ( diff --git a/awx_collection/plugins/modules/workflow_job_template_node.py b/awx_collection/plugins/modules/workflow_job_template_node.py index 61f713f843..a59cd33f9c 100644 --- a/awx_collection/plugins/modules/workflow_job_template_node.py +++ b/awx_collection/plugins/modules/workflow_job_template_node.py @@ -20,7 +20,6 @@ short_description: create, update, or destroy Automation Platform Controller wor description: - Create, update, or destroy Automation Platform Controller workflow job template nodes. - Use this to build a graph for a workflow, which dictates what the workflow runs. - - Replaces the deprecated tower_workflow_template module schema command. - You can create nodes first, and link them afterwards, and not worry about ordering. For failsafe referencing of a node, specify identifier, WFJT, and organization. With those specified, you can choose to modify or not modify any other parameter. diff --git a/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 b/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 index 246897f1b4..e860c43f8c 100644 --- a/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 +++ b/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 @@ -74,6 +74,7 @@ Notable releases of the `{{ collection_namespace }}.{{ collection_package }}` co - 7.0.0 is intended to be identical to the content prior to the migration, aside from changes necessary to function as a collection. - 11.0.0 has no non-deprecated modules that depend on the deprecated `tower-cli` [PyPI](https://pypi.org/project/ansible-tower-cli/). - 19.2.1 large renaming purged "tower" names (like options and module names), adding redirects for old names + - 21.11.0 "tower" modules deprecated and symlinks removed. - 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable. {% else %} - 3.7.0 initial release diff --git a/awxkit/README.md b/awxkit/README.md index e07d1622e5..6fdf56f132 100644 --- a/awxkit/README.md +++ b/awxkit/README.md @@ -1,6 +1,10 @@ awxkit ====== -Python library that backs the provided `awx` command line client. +A Python library that backs the provided `awx` command line client. -For more information on installing the CLI and building the docs on how to use it, look [here](./awxkit/cli/docs). +It can be installed by running `pip install awxkit`. + +The PyPI respository can be found [here](https://pypi.org/project/awxkit/). + +For more information on installing the CLI and building the docs on how to use it, look [here](./awxkit/cli/docs). \ No newline at end of file