diff --git a/awx/main/credential_plugins/conjur.py b/awx/main/credential_plugins/conjur.py index 313b766bdc..718eebbc64 100644 --- a/awx/main/credential_plugins/conjur.py +++ b/awx/main/credential_plugins/conjur.py @@ -50,9 +50,9 @@ conjur_inputs = { def conjur_backend(**kwargs): url = kwargs['url'] api_key = kwargs['api_key'] - account = quote(kwargs['account']) - username = quote(kwargs['username']) - secret_path = quote(kwargs['secret_path']) + account = quote(kwargs['account'], safe='') + username = quote(kwargs['username'], safe='') + secret_path = quote(kwargs['secret_path'], safe='') version = kwargs.get('secret_version') cacert = kwargs.get('cacert', None) diff --git a/awx/main/dispatch/worker/base.py b/awx/main/dispatch/worker/base.py index b0611676fa..7001cd9bb9 100644 --- a/awx/main/dispatch/worker/base.py +++ b/awx/main/dispatch/worker/base.py @@ -35,6 +35,7 @@ class WorkerSignalHandler: def __init__(self): self.kill_now = False + signal.signal(signal.SIGTERM, signal.SIG_DFL) signal.signal(signal.SIGINT, self.exit_gracefully) def exit_gracefully(self, *args, **kwargs): diff --git a/awx/main/management/commands/register_queue.py b/awx/main/management/commands/register_queue.py index 61761ec2aa..edd8068b89 100644 --- a/awx/main/management/commands/register_queue.py +++ b/awx/main/management/commands/register_queue.py @@ -16,31 +16,24 @@ class InstanceNotFound(Exception): super(InstanceNotFound, self).__init__(*args, **kwargs) -class Command(BaseCommand): +class RegisterQueue: + def __init__(self, queuename, controller, instance_percent, inst_min, hostname_list): + self.instance_not_found_err = None + self.queuename = queuename + self.controller = controller + self.instance_percent = instance_percent + self.instance_min = inst_min + self.hostname_list = hostname_list - def add_arguments(self, parser): - parser.add_argument('--queuename', dest='queuename', type=str, - help='Queue to create/update') - parser.add_argument('--hostnames', dest='hostnames', type=str, - help='Comma-Delimited Hosts to add to the Queue (will not remove already assigned instances)') - parser.add_argument('--controller', dest='controller', type=str, - default='', help='The controlling group (makes this an isolated group)') - parser.add_argument('--instance_percent', dest='instance_percent', type=int, default=0, - help='The percentage of active instances that will be assigned to this group'), - parser.add_argument('--instance_minimum', dest='instance_minimum', type=int, default=0, - help='The minimum number of instance that will be retained for this group from available instances') - - - def get_create_update_instance_group(self, queuename, instance_percent, instance_min): + def get_create_update_instance_group(self): created = False changed = False - - (ig, created) = InstanceGroup.objects.get_or_create(name=queuename) - if ig.policy_instance_percentage != instance_percent: - ig.policy_instance_percentage = instance_percent + (ig, created) = InstanceGroup.objects.get_or_create(name=self.queuename) + if ig.policy_instance_percentage != self.instance_percent: + ig.policy_instance_percentage = self.instance_percent changed = True - if ig.policy_instance_minimum != instance_min: - ig.policy_instance_minimum = instance_min + if ig.policy_instance_minimum != self.instance_min: + ig.policy_instance_minimum = self.instance_min changed = True if changed: @@ -48,12 +41,12 @@ class Command(BaseCommand): return (ig, created, changed) - def update_instance_group_controller(self, ig, controller): + def update_instance_group_controller(self, ig): changed = False control_ig = None - if controller: - control_ig = InstanceGroup.objects.filter(name=controller).first() + if self.controller: + control_ig = InstanceGroup.objects.filter(name=self.controller).first() if control_ig and ig.controller_id != control_ig.pk: ig.controller = control_ig @@ -62,10 +55,10 @@ class Command(BaseCommand): return (control_ig, changed) - def add_instances_to_group(self, ig, hostname_list): + def add_instances_to_group(self, ig): changed = False - instance_list_unique = set([x.strip() for x in hostname_list if x]) + instance_list_unique = set([x.strip() for x in self.hostname_list if x]) instances = [] for inst_name in instance_list_unique: instance = Instance.objects.filter(hostname=inst_name) @@ -86,43 +79,61 @@ class Command(BaseCommand): return (instances, changed) - def handle(self, **options): - instance_not_found_err = None - queuename = options.get('queuename') - if not queuename: - raise CommandError("Specify `--queuename` to use this command.") - ctrl = options.get('controller') - inst_per = options.get('instance_percent') - inst_min = options.get('instance_minimum') - hostname_list = [] - if options.get('hostnames'): - hostname_list = options.get('hostnames').split(",") - + def register(self): with advisory_lock('cluster_policy_lock'): with transaction.atomic(): changed2 = False changed3 = False - (ig, created, changed1) = self.get_create_update_instance_group(queuename, inst_per, inst_min) + (ig, created, changed1) = self.get_create_update_instance_group() if created: print("Creating instance group {}".format(ig.name)) elif not created: print("Instance Group already registered {}".format(ig.name)) - if ctrl: - (ig_ctrl, changed2) = self.update_instance_group_controller(ig, ctrl) + if self.controller: + (ig_ctrl, changed2) = self.update_instance_group_controller(ig) if changed2: - print("Set controller group {} on {}.".format(ctrl, queuename)) + print("Set controller group {} on {}.".format(self.controller, self.queuename)) try: - (instances, changed3) = self.add_instances_to_group(ig, hostname_list) + (instances, changed3) = self.add_instances_to_group(ig) for i in instances: print("Added instance {} to {}".format(i.hostname, ig.name)) except InstanceNotFound as e: - instance_not_found_err = e + self.instance_not_found_err = e if any([changed1, changed2, changed3]): print('(changed: True)') - if instance_not_found_err: - print(instance_not_found_err.message) + +class Command(BaseCommand): + + def add_arguments(self, parser): + parser.add_argument('--queuename', dest='queuename', type=str, + help='Queue to create/update') + parser.add_argument('--hostnames', dest='hostnames', type=str, + help='Comma-Delimited Hosts to add to the Queue (will not remove already assigned instances)') + parser.add_argument('--controller', dest='controller', type=str, + default='', help='The controlling group (makes this an isolated group)') + parser.add_argument('--instance_percent', dest='instance_percent', type=int, default=0, + help='The percentage of active instances that will be assigned to this group'), + parser.add_argument('--instance_minimum', dest='instance_minimum', type=int, default=0, + help='The minimum number of instance that will be retained for this group from available instances') + + + def handle(self, **options): + queuename = options.get('queuename') + if not queuename: + raise CommandError("Specify `--queuename` to use this command.") + ctrl = options.get('controller') + inst_per = options.get('instance_percent') + instance_min = options.get('instance_minimum') + hostname_list = [] + if options.get('hostnames'): + hostname_list = options.get('hostnames').split(",") + + rq = RegisterQueue(queuename, ctrl, inst_per, instance_min, hostname_list) + rq.register() + if rq.instance_not_found_err: + print(rq.instance_not_found_err.message) sys.exit(1) diff --git a/awx/main/managers.py b/awx/main/managers.py index 2076e7f0b0..f4b437d027 100644 --- a/awx/main/managers.py +++ b/awx/main/managers.py @@ -149,8 +149,11 @@ class InstanceManager(models.Manager): def get_or_register(self): if settings.AWX_AUTO_DEPROVISION_INSTANCES: + from awx.main.management.commands.register_queue import RegisterQueue pod_ip = os.environ.get('MY_POD_IP') - return self.register(ip_address=pod_ip) + registered = self.register(ip_address=pod_ip) + RegisterQueue('tower', None, 100, 0, []).register() + return registered else: return (False, self.me()) diff --git a/awx/main/notifications/mattermost_backend.py b/awx/main/notifications/mattermost_backend.py index 7a759d41a3..78a23c72d1 100644 --- a/awx/main/notifications/mattermost_backend.py +++ b/awx/main/notifications/mattermost_backend.py @@ -3,7 +3,6 @@ import logging import requests -import json from django.utils.encoding import smart_text from django.utils.translation import ugettext_lazy as _ @@ -45,7 +44,7 @@ class MattermostBackend(AWXBaseEmailBackend, CustomNotificationBase): payload['text'] = m.subject r = requests.post("{}".format(m.recipients()[0]), - data=json.dumps(payload), verify=(not self.mattermost_no_verify_ssl)) + json=payload, verify=(not self.mattermost_no_verify_ssl)) if r.status_code >= 400: logger.error(smart_text(_("Error sending notification mattermost: {}").format(r.text))) if not self.fail_silently: diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index 247f37544d..c16a1d1fa0 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -581,3 +581,4 @@ class TaskManager(): logger.debug("Starting Scheduler") with task_manager_bulk_reschedule(): self._schedule() + logger.debug("Finishing Scheduler") diff --git a/awx/main/utils/handlers.py b/awx/main/utils/handlers.py index ae0e83a9c5..c5e0014f8e 100644 --- a/awx/main/utils/handlers.py +++ b/awx/main/utils/handlers.py @@ -13,6 +13,10 @@ class RSysLogHandler(logging.handlers.SysLogHandler): append_nul = False + def _connect_unixsocket(self, address): + super(RSysLogHandler, self)._connect_unixsocket(address) + self.socket.setblocking(False) + def emit(self, msg): if not settings.LOG_AGGREGATOR_ENABLED: return @@ -26,6 +30,14 @@ class RSysLogHandler(logging.handlers.SysLogHandler): # unfortunately, we can't log that because...rsyslogd is down (and # would just us back ddown this code path) pass + except BlockingIOError: + # for , rsyslogd is no longer reading from the domain socket, and + # we're unable to write any more to it without blocking (we've seen this behavior + # from time to time when logging is totally misconfigured; + # in this scenario, it also makes more sense to just drop the messages, + # because the alternative is blocking the socket.send() in the + # Python process, which we definitely don't want to do) + pass ColorHandler = logging.StreamHandler diff --git a/awx/sso/fields.py b/awx/sso/fields.py index dddd1ee6a1..78f750fbbd 100644 --- a/awx/sso/fields.py +++ b/awx/sso/fields.py @@ -740,7 +740,9 @@ class SAMLOrgAttrField(HybridDictField): class SAMLTeamAttrTeamOrgMapField(HybridDictField): team = fields.CharField(required=True, allow_null=False) + team_alias = fields.CharField(required=False, allow_null=True) organization = fields.CharField(required=True, allow_null=False) + organization_alias = fields.CharField(required=False, allow_null=True) child = _Forbidden() diff --git a/awx/sso/pipeline.py b/awx/sso/pipeline.py index 212e3824ba..6d7e05da90 100644 --- a/awx/sso/pipeline.py +++ b/awx/sso/pipeline.py @@ -187,13 +187,22 @@ def update_user_teams_by_saml_attr(backend, details, user=None, *args, **kwargs) team_ids = [] for team_name_map in team_map.get('team_org_map', []): - team_name = team_name_map.get('team', '') + team_name = team_name_map.get('team', None) + team_alias = team_name_map.get('team_alias', None) + organization_name = team_name_map.get('organization', None) + organization_alias = team_name_map.get('organization_alias', None) if team_name in saml_team_names: - if not team_name_map.get('organization', ''): + if not organization_name: # Settings field validation should prevent this. logger.error("organization name invalid for team {}".format(team_name)) continue - org = Organization.objects.get_or_create(name=team_name_map['organization'])[0] + + if organization_alias: + organization_name = organization_alias + org = Organization.objects.get_or_create(name=organization_name)[0] + + if team_alias: + team_name = team_alias team = Team.objects.get_or_create(name=team_name, organization=org)[0] team_ids.append(team.id) diff --git a/awx/sso/tests/functional/test_pipeline.py b/awx/sso/tests/functional/test_pipeline.py index 78a04a0481..06d5503db8 100644 --- a/awx/sso/tests/functional/test_pipeline.py +++ b/awx/sso/tests/functional/test_pipeline.py @@ -193,6 +193,10 @@ class TestSAMLAttr(): {'team': 'Red', 'organization': 'Default1'}, {'team': 'Green', 'organization': 'Default1'}, {'team': 'Green', 'organization': 'Default3'}, + { + 'team': 'Yellow', 'team_alias': 'Yellow_Alias', + 'organization': 'Default4', 'organization_alias': 'Default4_Alias' + }, ] } return MockSettings() @@ -285,3 +289,18 @@ class TestSAMLAttr(): assert Team.objects.get(name='Green', organization__name='Default1').member_role.members.count() == 3 assert Team.objects.get(name='Green', organization__name='Default3').member_role.members.count() == 3 + def test_update_user_teams_alias_by_saml_attr(self, orgs, users, kwargs, mock_settings): + with mock.patch('django.conf.settings', mock_settings): + u1 = users[0] + + # Test getting teams from attribute with team->org mapping + kwargs['response']['attributes']['groups'] = ['Yellow'] + + # Ensure team and org will be created + update_user_teams_by_saml_attr(None, None, u1, **kwargs) + + assert Team.objects.filter(name='Yellow', organization__name='Default4').count() == 0 + assert Team.objects.filter(name='Yellow_Alias', organization__name='Default4_Alias').count() == 1 + assert Team.objects.get( + name='Yellow_Alias', organization__name='Default4_Alias').member_role.members.count() == 1 + diff --git a/awx/sso/tests/unit/test_fields.py b/awx/sso/tests/unit/test_fields.py index c63e137776..6d7505e022 100644 --- a/awx/sso/tests/unit/test_fields.py +++ b/awx/sso/tests/unit/test_fields.py @@ -71,6 +71,14 @@ class TestSAMLTeamAttrField(): {'team': 'Engineering', 'organization': 'Ansible2'}, {'team': 'Engineering2', 'organization': 'Ansible'}, ]}, + {'remove': True, 'saml_attr': 'foobar', 'team_org_map': [ + { + 'team': 'Engineering', 'team_alias': 'Engineering Team', + 'organization': 'Ansible', 'organization_alias': 'Awesome Org' + }, + {'team': 'Engineering', 'organization': 'Ansible2'}, + {'team': 'Engineering2', 'organization': 'Ansible'}, + ]}, ]) def test_internal_value_valid(self, data): field = SAMLTeamAttrField() diff --git a/awx/ui/package-lock.json b/awx/ui/package-lock.json index 6719e512e4..3e072d5294 100644 --- a/awx/ui/package-lock.json +++ b/awx/ui/package-lock.json @@ -14435,9 +14435,9 @@ } }, "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true }, "whet.extend": { diff --git a/awx/ui_next/package-lock.json b/awx/ui_next/package-lock.json index 57577d86d9..e9635701b6 100644 --- a/awx/ui_next/package-lock.json +++ b/awx/ui_next/package-lock.json @@ -16320,9 +16320,9 @@ } }, "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==" + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" }, "whatwg-encoding": { "version": "1.0.5", diff --git a/awx/ui_next/src/App.jsx b/awx/ui_next/src/App.jsx index e74edee190..ffafd07dba 100644 --- a/awx/ui_next/src/App.jsx +++ b/awx/ui_next/src/App.jsx @@ -55,12 +55,12 @@ function App() { {getRouteConfig(i18n) .flatMap(({ routes }) => routes) .map(({ path, screen: Screen }) => ( - + )) .concat( - + )} diff --git a/awx/ui_next/src/components/ClipboardCopyButton/ClipboardCopyButton.jsx b/awx/ui_next/src/components/ClipboardCopyButton/ClipboardCopyButton.jsx index 8fcac00468..f2f8b855d6 100644 --- a/awx/ui_next/src/components/ClipboardCopyButton/ClipboardCopyButton.jsx +++ b/awx/ui_next/src/components/ClipboardCopyButton/ClipboardCopyButton.jsx @@ -40,7 +40,13 @@ class ClipboardCopyButton extends React.Component { }; render() { - const { clickTip, entryDelay, exitDelay, hoverTip } = this.props; + const { + copyTip, + entryDelay, + exitDelay, + copiedSuccessTip, + isDisabled, + } = this.props; const { copied } = this.state; return ( @@ -48,12 +54,13 @@ class ClipboardCopyButton extends React.Component { entryDelay={entryDelay} exitDelay={exitDelay} trigger="mouseenter focus click" - content={copied ? clickTip : hoverTip} + content={copied ? copiedSuccessTip : copyTip} > @@ -63,12 +70,13 @@ class ClipboardCopyButton extends React.Component { } ClipboardCopyButton.propTypes = { - clickTip: PropTypes.string.isRequired, + copyTip: PropTypes.string.isRequired, entryDelay: PropTypes.number, exitDelay: PropTypes.number, - hoverTip: PropTypes.string.isRequired, + copiedSuccessTip: PropTypes.string.isRequired, stringToCopy: PropTypes.string.isRequired, switchDelay: PropTypes.number, + isDisabled: PropTypes.bool.isRequired, }; ClipboardCopyButton.defaultProps = { diff --git a/awx/ui_next/src/components/ClipboardCopyButton/ClipboardCopyButton.test.jsx b/awx/ui_next/src/components/ClipboardCopyButton/ClipboardCopyButton.test.jsx index 4ba32fb51d..35836c0634 100644 --- a/awx/ui_next/src/components/ClipboardCopyButton/ClipboardCopyButton.test.jsx +++ b/awx/ui_next/src/components/ClipboardCopyButton/ClipboardCopyButton.test.jsx @@ -13,6 +13,7 @@ describe('ClipboardCopyButton', () => { clickTip="foo" hoverTip="bar" stringToCopy="foobar!" + isDisabled={false} /> ); expect(wrapper).toHaveLength(1); @@ -23,6 +24,7 @@ describe('ClipboardCopyButton', () => { clickTip="foo" hoverTip="bar" stringToCopy="foobar!" + isDisabled={false} /> ).find('ClipboardCopyButton'); expect(wrapper.state('copied')).toBe(false); @@ -33,4 +35,15 @@ describe('ClipboardCopyButton', () => { wrapper.update(); expect(wrapper.state('copied')).toBe(false); }); + test('should render disabled button', () => { + const wrapper = mountWithContexts( + + ); + expect(wrapper.find('Button').prop('isDisabled')).toBe(true); + }); }); diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/PreviewStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/PreviewStep.jsx index 835fbba18d..14e26f4d10 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/PreviewStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/PreviewStep.jsx @@ -1,11 +1,29 @@ -import React from 'react'; +import React, { Fragment } from 'react'; +import styled from 'styled-components'; +import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons'; +import { Tooltip } from '@patternfly/react-core'; +import { t } from '@lingui/macro'; import { useFormikContext } from 'formik'; +import { withI18n } from '@lingui/react'; import yaml from 'js-yaml'; -import PromptDetail from '../../PromptDetail'; import mergeExtraVars, { maskPasswords } from '../mergeExtraVars'; import getSurveyValues from '../getSurveyValues'; +import PromptDetail from '../../PromptDetail'; -function PreviewStep({ resource, config, survey, formErrors }) { +const ExclamationCircleIcon = styled(PFExclamationCircleIcon)` + margin-left: 10px; + margin-top: -2px; +`; + +const ErrorMessageWrapper = styled.div` + align-items: center; + color: var(--pf-global--danger-color--200); + display: flex; + font-weight: var(--pf-global--FontWeight--bold); + margin-bottom: 10px; +`; + +function PreviewStep({ resource, config, survey, formErrors, i18n }) { const { values } = useFormikContext(); const surveyValues = getSurveyValues(values); @@ -29,21 +47,26 @@ function PreviewStep({ resource, config, survey, formErrors }) { } return ( - <> + + {formErrors.length > 0 && ( + + {i18n._(t`Some of the previous step(s) have errors`)} + + + + + )} - {formErrors && ( -
    - {Object.keys(formErrors).map( - field => `${field}: ${formErrors[field]}` - )} -
- )} - +
); } -export default PreviewStep; +export default withI18n()(PreviewStep); diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/PreviewStep.test.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/PreviewStep.test.jsx index 71a33b2fec..b596e866ea 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/PreviewStep.test.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/PreviewStep.test.jsx @@ -24,6 +24,10 @@ const survey = { ], }; +const formErrors = { + inventory: 'An inventory must be selected', +}; + describe('PreviewStep', () => { test('should render PromptDetail', async () => { let wrapper; @@ -37,6 +41,7 @@ describe('PreviewStep', () => { survey_enabled: true, }} survey={survey} + formErrors={formErrors} /> ); @@ -62,6 +67,7 @@ describe('PreviewStep', () => { config={{ ask_limit_on_launch: true, }} + formErrors={formErrors} /> ); @@ -85,6 +91,7 @@ describe('PreviewStep', () => { config={{ ask_variables_on_launch: true, }} + formErrors={formErrors} /> ); diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/useCredentialsStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/useCredentialsStep.jsx index f63d85599b..85299a08c1 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/useCredentialsStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/useCredentialsStep.jsx @@ -19,7 +19,8 @@ export default function useCredentialsStep( initialValues: getInitialValues(config, resource), validate, isReady: true, - error: null, + contentError: null, + formError: null, setTouched: setFieldsTouched => { setFieldsTouched({ credentials: true, diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx index cd1deb76f7..aa8acbd6f6 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx @@ -27,7 +27,8 @@ export default function useInventoryStep(config, resource, visitedSteps, i18n) { initialValues: getInitialValues(config, resource), validate, isReady: true, - error: null, + contentError: null, + formError: stepErrors, setTouched: setFieldsTouched => { setFieldsTouched({ inventory: true, diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/useOtherPromptsStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/useOtherPromptsStep.jsx index 516238ca7a..1d33987c92 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/useOtherPromptsStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/useOtherPromptsStep.jsx @@ -24,7 +24,8 @@ export default function useOtherPrompt(config, resource, visitedSteps, i18n) { initialValues: getInitialValues(config, resource), validate, isReady: true, - error: null, + contentError: null, + formError: stepErrors, setTouched: setFieldsTouched => { setFieldsTouched({ job_type: true, diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/useSurveyStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/useSurveyStep.jsx index 5ee623dd14..ac0fbe0c3c 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/useSurveyStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/useSurveyStep.jsx @@ -54,7 +54,8 @@ export default function useSurveyStep(config, resource, visitedSteps, i18n) { validate, survey, isReady: !isLoading && !!survey, - error, + contentError: error, + formError: stepErrors, setTouched: setFieldsTouched => { if (!survey || !survey.spec) { return; diff --git a/awx/ui_next/src/components/LaunchPrompt/useSteps.js b/awx/ui_next/src/components/LaunchPrompt/useSteps.js index ed61a01804..e8519c58a4 100644 --- a/awx/ui_next/src/components/LaunchPrompt/useSteps.js +++ b/awx/ui_next/src/components/LaunchPrompt/useSteps.js @@ -13,14 +13,13 @@ export default function useSteps(config, resource, i18n) { useOtherPromptsStep(config, resource, visited, i18n), useSurveyStep(config, resource, visited, i18n), ]; + + const formErrorsContent = steps + .filter(s => s?.formError && Object.keys(s.formError).length > 0) + .map(({ formError }) => formError); + steps.push( - usePreviewStep( - config, - resource, - steps[3].survey, - {}, // TODO: formErrors ? - i18n - ) + usePreviewStep(config, resource, steps[3].survey, formErrorsContent, i18n) ); const pfSteps = steps.map(s => s.step).filter(s => s != null); @@ -31,8 +30,9 @@ export default function useSteps(config, resource, i18n) { }; }, {}); const isReady = !steps.some(s => !s.isReady); - const stepWithError = steps.find(s => s.error); - const contentError = stepWithError ? stepWithError.error : null; + + const stepWithError = steps.find(s => s.contentError); + const contentError = stepWithError ? stepWithError.contentError : null; const validate = values => { const errors = steps.reduce((acc, cur) => { diff --git a/awx/ui_next/src/routeConfig.js b/awx/ui_next/src/routeConfig.js index 37f1732cb0..4b43dc4993 100644 --- a/awx/ui_next/src/routeConfig.js +++ b/awx/ui_next/src/routeConfig.js @@ -138,7 +138,7 @@ function getRouteConfig(i18n) { screen: InstanceGroups, }, { - title: i18n._(t`Integrations`), + title: i18n._(t`Applications`), path: '/applications', screen: Applications, }, diff --git a/awx/ui_next/src/screens/Application/Application/Application.jsx b/awx/ui_next/src/screens/Application/Application/Application.jsx new file mode 100644 index 0000000000..5a002f2990 --- /dev/null +++ b/awx/ui_next/src/screens/Application/Application/Application.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Route, Switch, Redirect } from 'react-router-dom'; +import ApplicationEdit from '../ApplicationEdit'; +import ApplicationDetails from '../ApplicationDetails'; + +function Application() { + return ( + <> + + + + + + + + + + + ); +} + +export default Application; diff --git a/awx/ui_next/src/screens/Application/Application/index.js b/awx/ui_next/src/screens/Application/Application/index.js new file mode 100644 index 0000000000..f76f133dd5 --- /dev/null +++ b/awx/ui_next/src/screens/Application/Application/index.js @@ -0,0 +1 @@ +export { default } from './Application'; diff --git a/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.jsx b/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.jsx new file mode 100644 index 0000000000..a25c690a7b --- /dev/null +++ b/awx/ui_next/src/screens/Application/ApplicationAdd/ApplicationAdd.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Card, PageSection } from '@patternfly/react-core'; + +function ApplicatonAdd() { + return ( + <> + + +
Applications Add
+
+
+ + ); +} +export default ApplicatonAdd; diff --git a/awx/ui_next/src/screens/Application/ApplicationAdd/index.js b/awx/ui_next/src/screens/Application/ApplicationAdd/index.js new file mode 100644 index 0000000000..54101fc16b --- /dev/null +++ b/awx/ui_next/src/screens/Application/ApplicationAdd/index.js @@ -0,0 +1 @@ +export { default } from './ApplicationAdd'; diff --git a/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx b/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx new file mode 100644 index 0000000000..c8051841bb --- /dev/null +++ b/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Card, PageSection } from '@patternfly/react-core'; + +function ApplicationDetails() { + return ( + + Application Details + + ); +} +export default ApplicationDetails; diff --git a/awx/ui_next/src/screens/Application/ApplicationDetails/index.js b/awx/ui_next/src/screens/Application/ApplicationDetails/index.js new file mode 100644 index 0000000000..fc3261983b --- /dev/null +++ b/awx/ui_next/src/screens/Application/ApplicationDetails/index.js @@ -0,0 +1 @@ +export { default } from './ApplicationDetails'; diff --git a/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx b/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx new file mode 100644 index 0000000000..e72f93b681 --- /dev/null +++ b/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Card, PageSection } from '@patternfly/react-core'; + +function ApplicationEdit() { + return ( + + Application Edit + + ); +} +export default ApplicationEdit; diff --git a/awx/ui_next/src/screens/Application/ApplicationEdit/index.js b/awx/ui_next/src/screens/Application/ApplicationEdit/index.js new file mode 100644 index 0000000000..2ab4beb8d4 --- /dev/null +++ b/awx/ui_next/src/screens/Application/ApplicationEdit/index.js @@ -0,0 +1 @@ +export { default } from './ApplicationEdit'; diff --git a/awx/ui_next/src/screens/Application/Applications.jsx b/awx/ui_next/src/screens/Application/Applications.jsx index 47a5e3250a..19a23be08e 100644 --- a/awx/ui_next/src/screens/Application/Applications.jsx +++ b/awx/ui_next/src/screens/Application/Applications.jsx @@ -1,26 +1,49 @@ -import React, { Component, Fragment } from 'react'; +import React, { useState, useCallback } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { - PageSection, - PageSectionVariants, - Title, -} from '@patternfly/react-core'; +import { Route, Switch } from 'react-router-dom'; -class Applications extends Component { - render() { - const { i18n } = this.props; - const { light } = PageSectionVariants; +import ApplicationsList from './ApplicationsList'; +import ApplicationAdd from './ApplicationAdd'; +import Application from './Application'; +import Breadcrumbs from '../../components/Breadcrumbs'; - return ( - - - {i18n._(t`Applications`)} - - - - ); - } +function Applications({ i18n }) { + const [breadcrumbConfig, setBreadcrumbConfig] = useState({ + '/applications': i18n._(t`Applications`), + '/applications/add': i18n._(t`Create New Application`), + }); + + const buildBreadcrumbConfig = useCallback( + application => { + if (!application) { + return; + } + + setBreadcrumbConfig({ + '/applications': i18n._(t`Applications`), + '/applications/add': i18n._(t`Create New Application`), + [`/application/${application.id}`]: `${application.name}`, + }); + }, + [i18n] + ); + return ( + <> + + + + + + + + + + + + + + ); } export default withI18n()(Applications); diff --git a/awx/ui_next/src/screens/Application/Applications.test.jsx b/awx/ui_next/src/screens/Application/Applications.test.jsx index cb747a920b..f309a2b60a 100644 --- a/awx/ui_next/src/screens/Application/Applications.test.jsx +++ b/awx/ui_next/src/screens/Application/Applications.test.jsx @@ -7,12 +7,10 @@ import Applications from './Applications'; describe('', () => { let pageWrapper; let pageSections; - let title; beforeEach(() => { pageWrapper = mountWithContexts(); pageSections = pageWrapper.find('PageSection'); - title = pageWrapper.find('Title'); }); afterEach(() => { @@ -21,9 +19,7 @@ describe('', () => { test('initially renders without crashing', () => { expect(pageWrapper.length).toBe(1); - expect(pageSections.length).toBe(2); - expect(title.length).toBe(1); - expect(title.props().size).toBe('2xl'); + expect(pageSections.length).toBe(1); expect(pageSections.first().props().variant).toBe('light'); }); }); diff --git a/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationsList.jsx b/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationsList.jsx new file mode 100644 index 0000000000..6fcf16bb73 --- /dev/null +++ b/awx/ui_next/src/screens/Application/ApplicationsList/ApplicationsList.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Card, PageSection } from '@patternfly/react-core'; + +function ApplicationsList() { + return ( + <> + + +
Applications List
+
+
+ + ); +} +export default ApplicationsList; diff --git a/awx/ui_next/src/screens/Application/ApplicationsList/index.js b/awx/ui_next/src/screens/Application/ApplicationsList/index.js new file mode 100644 index 0000000000..34f1107076 --- /dev/null +++ b/awx/ui_next/src/screens/Application/ApplicationsList/index.js @@ -0,0 +1 @@ +export { default } from './ApplicationsList'; diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx index 614062109f..d5651a130e 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx @@ -32,6 +32,11 @@ const DataListAction = styled(_DataListAction)` grid-gap: 16px; grid-template-columns: repeat(3, 40px); `; + +const Label = styled.span` + color: var(--pf-global--disabled-color--100); +`; + function ProjectListItem({ project, isSelected, @@ -121,13 +126,17 @@ function ProjectListItem({ , {project.scm_revision.substring(0, 7)} - {project.scm_revision ? ( - - ) : null} + {!project.scm_revision && ( + + )} + , ]} /> diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx index 9dfa8e8f1b..7866015703 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx @@ -218,4 +218,34 @@ describe('', () => { ); expect(wrapper.find('CopyButton').length).toBe(0); }); + test('should render disabled copy to clipboard button', () => { + const wrapper = mountWithContexts( + {}} + project={{ + id: 1, + name: 'Project 1', + url: '/api/v2/projects/1', + type: 'project', + scm_type: 'git', + scm_revision: '', + summary_fields: { + last_job: { + id: 9000, + status: 'successful', + }, + user_capabilities: { + edit: true, + }, + }, + }} + /> + ); + expect( + wrapper.find('span[aria-label="copy to clipboard disabled"]').text() + ).toBe('Sync for revision'); + expect(wrapper.find('ClipboardCopyButton').prop('isDisabled')).toBe(true); + }); }); diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index c16f861eb8..7bd4409fde 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { withFormik, useField, useFormikContext } from 'formik'; +import { withFormik, useField } from 'formik'; import { Form, FormGroup, @@ -52,8 +52,6 @@ function JobTemplateForm({ submitError, i18n, }) { - const { values: formikValues } = useFormikContext(); - const [contentError, setContentError] = useState(false); const [inventory, setInventory] = useState( template?.summary_fields?.inventory @@ -65,6 +63,7 @@ function JobTemplateForm({ Boolean(template.webhook_service) ); + const [askInventoryOnLaunchField] = useField('ask_inventory_on_launch'); const [jobTypeField, jobTypeMeta, jobTypeHelpers] = useField({ name: 'job_type', validate: required(null, i18n), @@ -81,7 +80,7 @@ function JobTemplateForm({ }); const [credentialField, , credentialHelpers] = useField('credentials'); const [labelsField, , labelsHelpers] = useField('labels'); - const [limitField, limitMeta] = useField('limit'); + const [limitField, limitMeta, limitHelpers] = useField('limit'); const [verbosityField] = useField('verbosity'); const [diffModeField, , diffModeHelpers] = useField('diff_mode'); const [instanceGroupsField, , instanceGroupsHelpers] = useField( @@ -231,7 +230,7 @@ function JobTemplateForm({ - {(inventoryMeta.touched || formikValues.ask_inventory_on_launch) && + {(inventoryMeta.touched || askInventoryOnLaunchField.value) && inventoryMeta.error && (
{ - scmField.onChange(event); + onChange={value => { + scmHelpers.setValue(value); }} /> @@ -383,8 +382,8 @@ function JobTemplateForm({ id="template-limit" {...limitField} isValid={!limitMeta.touched || !limitMeta.error} - onChange={(value, event) => { - limitField.onChange(event); + onChange={value => { + limitHelpers.setValue(value); }} /> diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx index aef6f61c18..a373fefe37 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx @@ -29,6 +29,7 @@ describe('', () => { playbook: 'Baz', type: 'job_template', scm_branch: 'Foo', + limit: '5000', summary_fields: { inventory: { id: 2, @@ -184,9 +185,10 @@ describe('', () => { wrapper.update(); await act(async () => { - wrapper.find('input#template-scm-branch').simulate('change', { - target: { value: 'devel', name: 'scm_branch' }, - }); + wrapper.find('TextInputBase#template-scm-branch').prop('onChange')( + 'devel' + ); + wrapper.find('TextInputBase#template-limit').prop('onChange')(1234567890); wrapper.find('AnsibleSelect[name="playbook"]').simulate('change', { target: { value: 'new baz type', name: 'playbook' }, }); @@ -221,6 +223,9 @@ describe('', () => { expect(wrapper.find('input#template-scm-branch').prop('value')).toEqual( 'devel' ); + expect(wrapper.find('input#template-limit').prop('value')).toEqual( + 1234567890 + ); expect( wrapper.find('AnsibleSelect[name="playbook"]').prop('value') ).toEqual('new baz type'); diff --git a/awx_collection/plugins/doc_fragments/auth.py b/awx_collection/plugins/doc_fragments/auth.py index 3763fafd80..1e77a63b4b 100644 --- a/awx_collection/plugins/doc_fragments/auth.py +++ b/awx_collection/plugins/doc_fragments/auth.py @@ -31,8 +31,11 @@ options: tower_oauthtoken: description: - The Tower OAuth token to use. + - This value can be in one of two formats. + - A string which is the token itself. (i.e. bqV5txm97wqJqtkxlMkhQz0pKhRMMX) + - A dictionary structure as returned by the tower_token module. - If value not set, will try environment variable C(TOWER_OAUTH_TOKEN) and then config files - type: str + type: raw version_added: "3.7" validate_certs: description: diff --git a/awx_collection/plugins/inventory/tower.py b/awx_collection/plugins/inventory/tower.py index 7ed641eefc..c906795a8e 100644 --- a/awx_collection/plugins/inventory/tower.py +++ b/awx_collection/plugins/inventory/tower.py @@ -12,7 +12,6 @@ DOCUMENTATION = ''' - Matthew Jones (@matburt) - Yunfan Zhang (@YunfanZhang42) short_description: Ansible dynamic inventory plugin for Ansible Tower. - version_added: "2.7" description: - Reads inventories from Ansible Tower. - Supports reading configuration from both YAML config file and environment variables. @@ -21,31 +20,23 @@ DOCUMENTATION = ''' are missing, this plugin will try to fill in missing arguments by reading from environment variables. - If reading configurations from environment variables, the path in the command must be @tower_inventory. options: - plugin: - description: the name of this plugin, it should always be set to 'tower' - for this plugin to recognize it as it's own. - env: - - name: ANSIBLE_INVENTORY_ENABLED - required: True - choices: ['tower'] host: description: The network address of your Ansible Tower host. - type: string env: - name: TOWER_HOST - required: True username: description: The user that you plan to use to access inventories on Ansible Tower. - type: string env: - name: TOWER_USERNAME - required: True password: description: The password for your Ansible Tower user. - type: string env: - name: TOWER_PASSWORD - required: True + oauth_token: + description: + - The Tower OAuth token to use. + env: + - name: TOWER_OAUTH_TOKEN inventory_id: description: - The ID of the Ansible Tower inventory that you wish to import. @@ -56,19 +47,18 @@ DOCUMENTATION = ''' env: - name: TOWER_INVENTORY required: True - validate_certs: - description: Specify whether Ansible should verify the SSL certificate of Ansible Tower host. + verify_ssl: + description: + - Specify whether Ansible should verify the SSL certificate of Ansible Tower host. + - Defaults to True, but this is handled by the shared module_utils code type: bool - default: True env: - name: TOWER_VERIFY_SSL - required: False - aliases: [ verify_ssl ] + aliases: [ validate_certs ] include_metadata: description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host. type: bool default: False - version_added: "2.8" ''' EXAMPLES = ''' @@ -99,7 +89,6 @@ inventory_id: the_ID_of_targeted_ansible_tower_inventory ''' import os -import re from ansible.module_utils import six from ansible.module_utils._text import to_text, to_native @@ -107,13 +96,11 @@ from ansible.errors import AnsibleParserError, AnsibleOptionsError from ansible.plugins.inventory import BaseInventoryPlugin from ansible.config.manager import ensure_type -from ..module_utils.ansible_tower import make_request, CollectionsParserError, Request +from ..module_utils.tower_api import TowerModule -# Python 2/3 Compatibility -try: - from urlparse import urljoin -except ImportError: - from urllib.parse import urljoin + +def handle_error(**kwargs): + raise AnsibleParserError(to_native(kwargs.get('msg'))) class InventoryModule(BaseInventoryPlugin): @@ -131,20 +118,25 @@ class InventoryModule(BaseInventoryPlugin): else: return False + def warn_callback(self, warning): + self.display.warning(warning) + def parse(self, inventory, loader, path, cache=True): super(InventoryModule, self).parse(inventory, loader, path) if not self.no_config_file_supplied and os.path.isfile(path): self._read_config_data(path) - # Read inventory from tower server. - # Note the environment variables will be handled automatically by InventoryManager. - tower_host = self.get_option('host') - if not re.match('(?:http|https)://', tower_host): - tower_host = 'https://{tower_host}'.format(tower_host=tower_host) - request_handler = Request(url_username=self.get_option('username'), - url_password=self.get_option('password'), - force_basic_auth=True, - validate_certs=self.get_option('validate_certs')) + # Defer processing of params to logic shared with the modules + module_params = {} + for plugin_param, module_param in TowerModule.short_params.items(): + opt_val = self.get_option(plugin_param) + if opt_val is not None: + module_params[module_param] = opt_val + + module = TowerModule( + argument_spec={}, direct_params=module_params, + error_callback=handle_error, warn_callback=self.warn_callback + ) # validate type of inventory_id because we allow two types as special case inventory_id = self.get_option('inventory_id') @@ -159,13 +151,11 @@ class InventoryModule(BaseInventoryPlugin): 'not integer, and cannot convert to string: {err}'.format(err=to_native(e)) ) inventory_id = inventory_id.replace('/', '') - inventory_url = '/api/v2/inventories/{inv_id}/script/?hostvars=1&towervars=1&all=1'.format(inv_id=inventory_id) - inventory_url = urljoin(tower_host, inventory_url) + inventory_url = '/api/v2/inventories/{inv_id}/script/'.format(inv_id=inventory_id) - try: - inventory = make_request(request_handler, inventory_url) - except CollectionsParserError as e: - raise AnsibleParserError(to_native(e)) + inventory = module.get_endpoint( + inventory_url, data={'hostvars': '1', 'towervars': '1', 'all': '1'} + )['json'] # To start with, create all the groups. for group_name in inventory: @@ -195,12 +185,8 @@ class InventoryModule(BaseInventoryPlugin): # Fetch extra variables if told to do so if self.get_option('include_metadata'): - config_url = urljoin(tower_host, '/api/v2/config/') - try: - config_data = make_request(request_handler, config_url) - except CollectionsParserError as e: - raise AnsibleParserError(to_native(e)) + config_data = module.get_endpoint('/api/v2/config/')['json'] server_data = {} server_data['license_type'] = config_data.get('license_info', {}).get('license_type', 'unknown') diff --git a/awx_collection/plugins/lookup/tower_schedule_rrule.py b/awx_collection/plugins/lookup/tower_schedule_rrule.py index e34751d9ac..0af71b0000 100644 --- a/awx_collection/plugins/lookup/tower_schedule_rrule.py +++ b/awx_collection/plugins/lookup/tower_schedule_rrule.py @@ -6,7 +6,6 @@ __metaclass__ = type DOCUMENTATION = """ lookup: tower_schedule_rrule author: John Westcott IV (@john-westcott-iv) - version_added: "3.7" short_description: Generate an rrule string which can be used for Tower Schedules requirements: - pytz diff --git a/awx_collection/plugins/module_utils/ansible_tower.py b/awx_collection/plugins/module_utils/ansible_tower.py index c51b410127..17d6a38680 100644 --- a/awx_collection/plugins/module_utils/ansible_tower.py +++ b/awx_collection/plugins/module_utils/ansible_tower.py @@ -29,14 +29,9 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import json import os import traceback -from ansible.module_utils._text import to_native -from ansible.module_utils.urls import urllib_error, ConnectionError, socket, httplib -from ansible.module_utils.urls import Request # noqa - TOWER_CLI_IMP_ERR = None try: import tower_cli.utils.exceptions as exc @@ -51,31 +46,6 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule, missing_required_lib -class CollectionsParserError(Exception): - pass - - -def make_request(request_handler, tower_url): - ''' - Makes the request to given URL, handles errors, returns JSON - ''' - try: - response = request_handler.get(tower_url) - except (ConnectionError, urllib_error.URLError, socket.error, httplib.HTTPException) as e: - n_error_msg = 'Connection to remote host failed: {err}'.format(err=to_native(e)) - # If Tower gives a readable error message, display that message to the user. - if callable(getattr(e, 'read', None)): - n_error_msg += ' with message: {err_msg}'.format(err_msg=to_native(e.read())) - raise CollectionsParserError(n_error_msg) - - # Attempt to parse JSON. - try: - return json.loads(response.read()) - except (ValueError, TypeError) as e: - # If the JSON parse fails, print the ValueError - raise CollectionsParserError('Failed to parse json from host: {err}'.format(err=to_native(e))) - - def tower_auth_config(module): ''' `tower_auth_config` attempts to load the tower-cli.cfg file diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index f22f6ed511..d836c457c0 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -3,7 +3,7 @@ __metaclass__ = type from ansible.module_utils.basic import AnsibleModule, env_fallback from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError -from ansible.module_utils.six import PY2 +from ansible.module_utils.six import PY2, string_types from ansible.module_utils.six.moves import StringIO from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode from ansible.module_utils.six.moves.urllib.error import HTTPError @@ -42,7 +42,21 @@ class TowerModule(AnsibleModule): 'tower': 'Red Hat Ansible Tower', } url = None - honorred_settings = ('host', 'username', 'password', 'verify_ssl', 'oauth_token') + AUTH_ARGSPEC = dict( + tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])), + tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), + tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])), + validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])), + tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), + tower_config_file=dict(type='path', required=False, default=None), + ) + short_params = { + 'host': 'tower_host', + 'username': 'tower_username', + 'password': 'tower_password', + 'verify_ssl': 'validate_certs', + 'oauth_token': 'tower_oauthtoken', + } host = '127.0.0.1' username = None password = None @@ -55,36 +69,46 @@ class TowerModule(AnsibleModule): config_name = 'tower_cli.cfg' ENCRYPTED_STRING = "$encrypted$" version_checked = False + error_callback = None + warn_callback = None - def __init__(self, argument_spec, **kwargs): - args = dict( - tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])), - tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), - tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])), - validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])), - tower_oauthtoken=dict(type='str', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), - tower_config_file=dict(type='path', required=False, default=None), - ) - args.update(argument_spec) + def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs): + full_argspec = {} + full_argspec.update(TowerModule.AUTH_ARGSPEC) + full_argspec.update(argument_spec) kwargs['supports_check_mode'] = True + self.error_callback = error_callback + self.warn_callback = warn_callback + self.json_output = {'changed': False} - super(TowerModule, self).__init__(argument_spec=args, **kwargs) + if direct_params is not None: + self.params = direct_params + else: + super(TowerModule, self).__init__(argument_spec=full_argspec, **kwargs) self.load_config_files() # Parameters specified on command line will override settings in any config - if self.params.get('tower_host'): - self.host = self.params.get('tower_host') - if self.params.get('tower_username'): - self.username = self.params.get('tower_username') - if self.params.get('tower_password'): - self.password = self.params.get('tower_password') - if self.params.get('validate_certs') is not None: - self.verify_ssl = self.params.get('validate_certs') + for short_param, long_param in self.short_params.items(): + direct_value = self.params.get(long_param) + if direct_value is not None: + setattr(self, short_param, direct_value) + + # Perform magic depending on whether tower_oauthtoken is a string or a dict if self.params.get('tower_oauthtoken'): - self.oauth_token = self.params.get('tower_oauthtoken') + token_param = self.params.get('tower_oauthtoken') + if type(token_param) is dict: + if 'token' in token_param: + self.oauth_token = self.params.get('tower_oauthtoken')['token'] + else: + self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry") + elif isinstance(token_param, string_types): + self.oauth_token = self.params.get('tower_oauthtoken') + else: + error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__) + self.fail_json(msg=error_msg) # Perform some basic validation if not re.match('^https{0,1}://', self.host): @@ -116,10 +140,10 @@ class TowerModule(AnsibleModule): # If we have a specified tower config, load it if self.params.get('tower_config_file'): - duplicated_params = [] - for direct_field in ('tower_host', 'tower_username', 'tower_password', 'validate_certs', 'tower_oauthtoken'): - if self.params.get(direct_field): - duplicated_params.append(direct_field) + duplicated_params = [ + fn for fn in self.AUTH_ARGSPEC + if fn != 'tower_config_file' and self.params.get(fn) is not None + ] if duplicated_params: self.warn(( 'The parameter(s) {0} were provided at the same time as tower_config_file. ' @@ -184,7 +208,7 @@ class TowerModule(AnsibleModule): # If we made it here then we have values from reading the ini file, so let's pull them out into a dict config_data = {} - for honorred_setting in self.honorred_settings: + for honorred_setting in self.short_params: try: config_data[honorred_setting] = config.get('general', honorred_setting) except NoOptionError: @@ -197,7 +221,7 @@ class TowerModule(AnsibleModule): raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e)) # If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here - for honorred_setting in self.honorred_settings: + for honorred_setting in self.short_params: if honorred_setting in config_data: # Veriffy SSL must be a boolean if honorred_setting == 'verify_ssl': @@ -494,6 +518,9 @@ class TowerModule(AnsibleModule): item_name = existing_item['username'] elif 'identifier' in existing_item: item_name = existing_item['identifier'] + elif item_type == 'o_auth2_access_token': + # An oauth2 token has no name, instead we will use its id for any of the messages + item_name = existing_item['id'] else: self.fail_json(msg="Unable to process delete of {0} due to missing name".format(item_type)) @@ -748,13 +775,22 @@ class TowerModule(AnsibleModule): def fail_json(self, **kwargs): # Try to log out if we are authenticated self.logout() - super(TowerModule, self).fail_json(**kwargs) + if self.error_callback: + self.error_callback(**kwargs) + else: + super(TowerModule, self).fail_json(**kwargs) def exit_json(self, **kwargs): # Try to log out if we are authenticated self.logout() super(TowerModule, self).exit_json(**kwargs) + def warn(self, warning): + if self.warn_callback is not None: + self.warn_callback(warning) + else: + super(TowerModule, self).warn(warning) + def is_job_done(self, job_status): if job_status in ['new', 'pending', 'waiting', 'running']: return False diff --git a/awx_collection/plugins/modules/tower_credential.py b/awx_collection/plugins/modules/tower_credential.py index 12a6e930ff..2ca50dea4d 100644 --- a/awx_collection/plugins/modules/tower_credential.py +++ b/awx_collection/plugins/modules/tower_credential.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_credential author: "Wayne Witzel III (@wwitzel3)" -version_added: "2.3" short_description: create, update, or destroy Ansible Tower credential. description: - Create, update, or destroy Ansible Tower credentials. See @@ -45,7 +44,6 @@ options: description: - Name of credential type. - Will be preferred over kind - version_added: "2.10" type: str inputs: description: @@ -53,7 +51,6 @@ options: Credential inputs where the keys are var names used in templating. Refer to the Ansible Tower documentation for example syntax. - Any fields in this dict will take prescedence over any fields mentioned below (i.e. host, username, etc) - version_added: "2.9" type: dict user: description: @@ -124,7 +121,6 @@ options: description: - STS token for aws type. - Deprecated, please use inputs - version_added: "2.6" type: str secret: description: @@ -177,7 +173,6 @@ options: - This parameter is only valid if C(kind) is specified as C(vault). - Deprecated, please use inputs type: str - version_added: "2.8" state: description: - Desired state of the resource. @@ -360,9 +355,9 @@ def main(): # Deprication warnings for legacy_input in OLD_INPUT_NAMES: if module.params.get(legacy_input) is not None: - module.deprecate(msg='{0} parameter has been deprecated, please use inputs instead'.format(legacy_input), version="3.6") + module.deprecate(msg='{0} parameter has been deprecated, please use inputs instead'.format(legacy_input), version="ansible.tower:4.0.0") if kind: - module.deprecate(msg='The kind parameter has been deprecated, please use credential_type instead', version="3.6") + module.deprecate(msg='The kind parameter has been deprecated, please use credential_type instead', version="ansible.tower:4.0.0") cred_type_id = module.resolve_name_to_id('credential_types', credential_type if credential_type else KIND_CHOICES[kind]) if organization: diff --git a/awx_collection/plugins/modules/tower_credential_type.py b/awx_collection/plugins/modules/tower_credential_type.py index 57ae60c6c3..561ae78f5a 100644 --- a/awx_collection/plugins/modules/tower_credential_type.py +++ b/awx_collection/plugins/modules/tower_credential_type.py @@ -18,7 +18,6 @@ DOCUMENTATION = ''' --- module: tower_credential_type author: "Adrien Fleury (@fleu42)" -version_added: "2.7" short_description: Create, update, or destroy custom Ansible Tower credential type. description: - Create, update, or destroy Ansible Tower credential type. See diff --git a/awx_collection/plugins/modules/tower_group.py b/awx_collection/plugins/modules/tower_group.py index e8ce833cca..a64826eb88 100644 --- a/awx_collection/plugins/modules/tower_group.py +++ b/awx_collection/plugins/modules/tower_group.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_group author: "Wayne Witzel III (@wwitzel3)" -version_added: "2.3" short_description: create, update, or destroy Ansible Tower group. description: - Create, update, or destroy Ansible Tower groups. See @@ -63,7 +62,6 @@ options: description: - A new name for this group (for renaming) type: str - version_added: "3.7" extends_documentation_fragment: awx.awx.auth ''' diff --git a/awx_collection/plugins/modules/tower_host.py b/awx_collection/plugins/modules/tower_host.py index 3cefc3bad6..cb4712a27c 100644 --- a/awx_collection/plugins/modules/tower_host.py +++ b/awx_collection/plugins/modules/tower_host.py @@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' --- module: tower_host -version_added: "2.3" author: "Wayne Witzel III (@wwitzel3)" short_description: create, update, or destroy Ansible Tower host. description: @@ -32,7 +31,6 @@ options: description: - To use when changing a hosts's name. type: str - version_added: "3.7" description: description: - The description to use for the host. diff --git a/awx_collection/plugins/modules/tower_inventory.py b/awx_collection/plugins/modules/tower_inventory.py index bfada2596e..d0ced63048 100644 --- a/awx_collection/plugins/modules/tower_inventory.py +++ b/awx_collection/plugins/modules/tower_inventory.py @@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' --- module: tower_inventory -version_added: "2.3" author: "Wayne Witzel III (@wwitzel3)" short_description: create, update, or destroy Ansible Tower inventory. description: @@ -46,12 +45,10 @@ options: - The kind field. Cannot be modified after created. default: "" choices: ["", "smart"] - version_added: "2.7" type: str host_filter: description: - The host_filter field. Only useful when C(kind=smart). - version_added: "2.7" type: str state: description: diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index c17751fa96..98d523837e 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_inventory_source author: "Adrien Fleury (@fleu42)" -version_added: "2.7" short_description: create, update, or destroy Ansible Tower inventory source. description: - Create, update, or destroy Ansible Tower inventory source. See @@ -32,7 +31,6 @@ options: description: - A new name for this assets (will rename the asset) type: str - version_added: "3.7" description: description: - The description to use for the inventory source. @@ -85,7 +83,6 @@ options: - Override vars in child groups and hosts with those from external source. type: bool custom_virtualenv: - version_added: "2.9" description: - Local absolute file path containing a custom Python virtualenv to use. type: str diff --git a/awx_collection/plugins/modules/tower_job_cancel.py b/awx_collection/plugins/modules/tower_job_cancel.py index c88ce3e935..5e82834f6c 100644 --- a/awx_collection/plugins/modules/tower_job_cancel.py +++ b/awx_collection/plugins/modules/tower_job_cancel.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_job_cancel author: "Wayne Witzel III (@wwitzel3)" -version_added: "2.3" short_description: Cancel an Ansible Tower Job. description: - Cancel Ansible Tower jobs. See diff --git a/awx_collection/plugins/modules/tower_job_launch.py b/awx_collection/plugins/modules/tower_job_launch.py index 325e7fcfbd..f3447bf24c 100644 --- a/awx_collection/plugins/modules/tower_job_launch.py +++ b/awx_collection/plugins/modules/tower_job_launch.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_job_launch author: "Wayne Witzel III (@wwitzel3)" -version_added: "2.3" short_description: Launch an Ansible Job. description: - Launch an Ansible Tower jobs. See @@ -64,29 +63,24 @@ options: - A specific of the SCM project to run the template on. - This is only applicable if your project allows for branch override. type: str - version_added: "3.7" skip_tags: description: - Specific tags to skip from the playbook. type: list elements: str - version_added: "3.7" verbosity: description: - Verbosity level for this job run type: int choices: [ 0, 1, 2, 3, 4, 5 ] - version_added: "3.7" diff_mode: description: - Show the changes made by Ansible tasks where supported type: bool - version_added: "3.7" credential_passwords: description: - Passwords for credentials which are set to prompt on launch type: dict - version_added: "3.7" extends_documentation_fragment: awx.awx.auth ''' diff --git a/awx_collection/plugins/modules/tower_job_list.py b/awx_collection/plugins/modules/tower_job_list.py index 9723fa774f..2ecfd9d98a 100644 --- a/awx_collection/plugins/modules/tower_job_list.py +++ b/awx_collection/plugins/modules/tower_job_list.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_job_list author: "Wayne Witzel III (@wwitzel3)" -version_added: "2.3" short_description: List Ansible Tower jobs. description: - List Ansible Tower jobs. See diff --git a/awx_collection/plugins/modules/tower_job_template.py b/awx_collection/plugins/modules/tower_job_template.py index 946ffdf5e5..1f1d776f28 100644 --- a/awx_collection/plugins/modules/tower_job_template.py +++ b/awx_collection/plugins/modules/tower_job_template.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_job_template author: "Wayne Witzel III (@wwitzel3)" -version_added: "2.3" short_description: create, update, or destroy Ansible Tower job templates. description: - Create, update, or destroy Ansible Tower job templates. See @@ -45,6 +44,14 @@ options: description: - Name of the inventory to use for the job template. type: str + organization: + description: + - Organization the job template exists in. + - Used to help lookup the object, cannot be modified using this module. + - The Organization is inferred from the associated project + - If not provided, will lookup by name only, which does not work with duplicates. + - Requires Tower Version 3.7.0 or AWX 10.0.0 IS NOT backwards compatible with earlier versions. + type: str project: description: - Name of the project to use for the job template. @@ -57,19 +64,16 @@ options: description: - Name of the credential to use for the job template. - Deprecated, use 'credentials'. - version_added: 2.7 type: str credentials: description: - List of credentials to use for the job template. type: list elements: str - version_added: 2.8 vault_credential: description: - Name of the vault credential to use for the job template. - Deprecated, use 'credentials'. - version_added: 2.7 type: str forks: description: @@ -89,7 +93,6 @@ options: description: - Specify C(extra_vars) for the template. type: dict - version_added: 3.7 job_tags: description: - Comma separated list of the tags to use for the job template. @@ -97,7 +100,6 @@ options: force_handlers: description: - Enable forcing playbook handlers to run even if a task fails. - version_added: 2.7 type: bool default: 'no' aliases: @@ -109,12 +111,10 @@ options: start_at_task: description: - Start the playbook at the task matching this name. - version_added: 2.7 type: str diff_mode: description: - Enable diff mode for the job template. - version_added: 2.7 type: bool aliases: - diff_mode_enabled @@ -122,7 +122,6 @@ options: use_fact_cache: description: - Enable use of fact caching for the job template. - version_added: 2.7 type: bool default: 'no' aliases: @@ -139,7 +138,6 @@ options: ask_diff_mode_on_launch: description: - Prompt user to enable diff mode (show changes) to files when supported by modules. - version_added: 2.7 type: bool default: 'False' aliases: @@ -154,7 +152,6 @@ options: ask_limit_on_launch: description: - Prompt user for a limit on launch. - version_added: 2.7 type: bool default: 'False' aliases: @@ -169,7 +166,6 @@ options: ask_skip_tags_on_launch: description: - Prompt user for job tags to skip on launch. - version_added: 2.7 type: bool default: 'False' aliases: @@ -184,7 +180,6 @@ options: ask_verbosity_on_launch: description: - Prompt user to choose a verbosity level on launch. - version_added: 2.7 type: bool default: 'False' aliases: @@ -206,13 +201,11 @@ options: survey_enabled: description: - Enable a survey on the job template. - version_added: 2.7 type: bool default: 'no' survey_spec: description: - JSON/YAML dict formatted survey definition. - version_added: 2.8 type: dict become_enabled: description: @@ -222,7 +215,6 @@ options: allow_simultaneous: description: - Allow simultaneous runs of the job template. - version_added: 2.7 type: bool default: 'no' aliases: @@ -232,7 +224,6 @@ options: - Maximum time in seconds to wait for a job to finish (server-side). type: int custom_virtualenv: - version_added: "2.9" description: - Local absolute file path containing a custom Python virtualenv to use. type: str @@ -299,6 +290,7 @@ EXAMPLES = ''' tower_job_template: name: "Ping" job_type: "run" + organization: "Default" inventory: "Local" project: "Demo" playbook: "ping.yml" @@ -349,6 +341,7 @@ def main(): name=dict(required=True), new_name=dict(), description=dict(default=''), + organization=dict(), job_type=dict(choices=['run', 'check']), inventory=dict(), project=dict(), @@ -415,19 +408,24 @@ def main(): credentials = [] credentials.append(credential) + new_fields = {} + search_fields = {'name': name} + + # Attempt to look up the related items the user specified (these will fail the module if not found) + organization_id = None + organization = module.params.get('organization') + if organization: + organization_id = module.resolve_name_to_id('organizations', organization) + search_fields['organization'] = new_fields['organization'] = organization_id + # Attempt to look up an existing item based on the provided data - existing_item = module.get_one('job_templates', **{ - 'data': { - 'name': name, - } - }) + existing_item = module.get_one('job_templates', **{'data': search_fields}) if state == 'absent': # If the state was absent we can let the module delete it if needed, the module will handle exiting from this module.delete_if_needed(existing_item) # Create the data that gets sent for create and update - new_fields = {} new_fields['name'] = new_name if new_name else name for field_name in ( 'description', 'job_type', 'playbook', 'scm_branch', 'forks', 'limit', 'verbosity', @@ -454,7 +452,20 @@ def main(): if inventory is not None: new_fields['inventory'] = module.resolve_name_to_id('inventories', inventory) if project is not None: - new_fields['project'] = module.resolve_name_to_id('projects', project) + if organization_id is not None: + project_data = module.get_one('projects', **{ + 'data': { + 'name': project, + 'organization': organization_id, + } + }) + if project_data is None: + module.fail_json(msg="The project {0} in organization {1} was not found on the Tower server".format( + project, organization + )) + new_fields['project'] = project_data['id'] + else: + new_fields['project'] = module.resolve_name_to_id('projects', project) if webhook_credential is not None: new_fields['webhook_credential'] = module.resolve_name_to_id('credentials', webhook_credential) diff --git a/awx_collection/plugins/modules/tower_job_wait.py b/awx_collection/plugins/modules/tower_job_wait.py index 6e954db561..77a6977c5c 100644 --- a/awx_collection/plugins/modules/tower_job_wait.py +++ b/awx_collection/plugins/modules/tower_job_wait.py @@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' --- module: tower_job_wait -version_added: "2.3" author: "Wayne Witzel III (@wwitzel3)" short_description: Wait for Ansible Tower job to finish. description: @@ -141,7 +140,7 @@ def main(): interval = abs((min_interval + max_interval) / 2) module.deprecate( msg="Min and max interval have been deprecated, please use interval instead; interval will be set to {0}".format(interval), - version="3.7" + version="ansible.tower:4.0.0" ) # Attempt to look up job based on the provided id diff --git a/awx_collection/plugins/modules/tower_label.py b/awx_collection/plugins/modules/tower_label.py index 0d3a5a2dad..d0820d93a8 100644 --- a/awx_collection/plugins/modules/tower_label.py +++ b/awx_collection/plugins/modules/tower_label.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_label author: "Wayne Witzel III (@wwitzel3)" -version_added: "2.3" short_description: create, update, or destroy Ansible Tower labels. description: - Create, update, or destroy Ansible Tower labels. See diff --git a/awx_collection/plugins/modules/tower_license.py b/awx_collection/plugins/modules/tower_license.py index 8f3a806720..a1d9840d50 100644 --- a/awx_collection/plugins/modules/tower_license.py +++ b/awx_collection/plugins/modules/tower_license.py @@ -16,7 +16,6 @@ DOCUMENTATION = ''' --- module: tower_license author: "John Westcott IV (@john-westcott-iv)" -version_added: "2.9" short_description: Set the license for Ansible Tower description: - Get or Set Ansible Tower license. See @@ -27,13 +26,11 @@ options: - The contents of the license file required: True type: dict - version_added: "3.7" eula_accepted: description: - Whether or not the EULA is accepted. required: True type: bool - version_added: "3.7" extends_documentation_fragment: awx.awx.auth ''' diff --git a/awx_collection/plugins/modules/tower_notification.py b/awx_collection/plugins/modules/tower_notification.py index c4bfda164c..bfe672a50e 100644 --- a/awx_collection/plugins/modules/tower_notification.py +++ b/awx_collection/plugins/modules/tower_notification.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_notification author: "Samuel Carpentier (@samcarpentier)" -version_added: "2.8" short_description: create, update, or destroy Ansible Tower notification. description: - Create, update, or destroy Ansible Tower notifications. See @@ -371,7 +370,9 @@ def main(): # Deprecation warnings for all other params for legacy_input in OLD_INPUT_NAMES: if module.params.get(legacy_input) is not None: - module.deprecate(msg='{0} parameter has been deprecated, please use notification_configuration instead'.format(legacy_input), version="3.6") + module.deprecate( + msg='{0} parameter has been deprecated, please use notification_configuration instead'.format(legacy_input), + version="ansible.tower:4.0.0") # Attempt to look up the related items the user specified (these will fail the module if not found) organization_id = None diff --git a/awx_collection/plugins/modules/tower_organization.py b/awx_collection/plugins/modules/tower_organization.py index 71238e1681..fbbbf2885c 100644 --- a/awx_collection/plugins/modules/tower_organization.py +++ b/awx_collection/plugins/modules/tower_organization.py @@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' --- module: tower_organization -version_added: "2.3" author: "Wayne Witzel III (@wwitzel3)" short_description: create, update, or destroy Ansible Tower organizations description: @@ -33,7 +32,6 @@ options: - The description to use for the organization. type: str custom_virtualenv: - version_added: "2.9" description: - Local absolute file path containing a custom Python virtualenv to use. type: str @@ -43,7 +41,6 @@ options: - The max hosts allowed in this organizations default: "0" type: int - version_added: "3.7" state: description: - Desired state of the resource. diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py index 5ef35539e8..12f9e2809c 100644 --- a/awx_collection/plugins/modules/tower_project.py +++ b/awx_collection/plugins/modules/tower_project.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_project author: "Wayne Witzel III (@wwitzel3)" -version_added: "2.3" short_description: create, update, or destroy Ansible Tower projects description: - Create, update, or destroy Ansible Tower projects. See @@ -56,7 +55,6 @@ options: - The refspec to use for the SCM resource. type: str default: '' - version_added: "3.7" scm_credential: description: - Name of the credential to use with this SCM resource. @@ -77,7 +75,6 @@ options: type: bool default: 'no' scm_update_cache_timeout: - version_added: "2.8" description: - Cache Timeout to cache prior project syncs for a certain number of seconds. Only valid if scm_update_on_launch is to True, otherwise ignored. @@ -87,17 +84,14 @@ options: description: - Allow changing the SCM branch or revision in a job template that uses this project. type: bool - version_added: "3.7" aliases: - scm_allow_override job_timeout: - version_added: "2.8" description: - The amount of time (in seconds) to run before the SCM Update is canceled. A value of 0 means no timeout. default: 0 type: int custom_virtualenv: - version_added: "2.8" description: - Local absolute file path containing a custom Python virtualenv to use type: str diff --git a/awx_collection/plugins/modules/tower_receive.py b/awx_collection/plugins/modules/tower_receive.py index bdce881ba7..b673e9b81d 100644 --- a/awx_collection/plugins/modules/tower_receive.py +++ b/awx_collection/plugins/modules/tower_receive.py @@ -17,11 +17,10 @@ DOCUMENTATION = ''' --- module: tower_receive deprecated: - removed_in: "3.7" + removed_in: "14.0.0" why: Deprecated in favor of upcoming C(_export) module. alternative: Once published, use M(tower_export) instead. author: "John Westcott IV (@john-westcott-iv)" -version_added: "2.8" short_description: Receive assets from Ansible Tower. description: - Receive assets from Ansible Tower. See @@ -166,7 +165,7 @@ def main(): module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) - module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI export command.", version="3.7") + module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI export command.", version="awx.awx:14.0.0") if not HAS_TOWER_CLI: module.fail_json(msg='ansible-tower-cli required for this module') diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index 2b23a85004..4cba215b0a 100644 --- a/awx_collection/plugins/modules/tower_role.py +++ b/awx_collection/plugins/modules/tower_role.py @@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' --- module: tower_role -version_added: "2.3" author: "Wayne Witzel III (@wwitzel3)" short_description: grant or revoke an Ansible Tower role. description: diff --git a/awx_collection/plugins/modules/tower_schedule.py b/awx_collection/plugins/modules/tower_schedule.py index b492a1654b..24f8468e4a 100644 --- a/awx_collection/plugins/modules/tower_schedule.py +++ b/awx_collection/plugins/modules/tower_schedule.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_schedule author: "John Westcott IV (@john-westcott-iv)" -version_added: "2.3" short_description: create, update, or destroy Ansible Tower schedules. description: - Create, update, or destroy Ansible Tower schedules. See diff --git a/awx_collection/plugins/modules/tower_send.py b/awx_collection/plugins/modules/tower_send.py index 6747a27002..7ac60ece59 100644 --- a/awx_collection/plugins/modules/tower_send.py +++ b/awx_collection/plugins/modules/tower_send.py @@ -17,11 +17,10 @@ DOCUMENTATION = ''' --- module: tower_send deprecated: - removed_in: "3.7" + removed_in: "14.0.0" why: Deprecated in favor of upcoming C(_import) module. alternative: Once published, use M(tower_import) instead. author: "John Westcott IV (@john-westcott-iv)" -version_added: "2.8" short_description: Send assets to Ansible Tower. description: - Send assets to Ansible Tower. See @@ -106,7 +105,7 @@ def main(): module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) - module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI import command", version="3.7") + module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI import command", version="awx.awx:14.0.0") if not HAS_TOWER_CLI: module.fail_json(msg='ansible-tower-cli required for this module') diff --git a/awx_collection/plugins/modules/tower_settings.py b/awx_collection/plugins/modules/tower_settings.py index b7ecde45ef..9db41d9975 100644 --- a/awx_collection/plugins/modules/tower_settings.py +++ b/awx_collection/plugins/modules/tower_settings.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_settings author: "Nikhil Jain (@jainnikhil30)" -version_added: "2.7" short_description: Modify Ansible Tower settings. description: - Modify Ansible Tower settings. See @@ -37,7 +36,6 @@ options: description: - A data structure to be sent into the settings endpoint type: dict - version_added: "3.7" requirements: - pyyaml extends_documentation_fragment: awx.awx.auth @@ -82,6 +80,10 @@ except ImportError: def coerce_type(module, value): + # If our value is already None we can just return directly + if value is None: + return value + yaml_ish = bool(( value.startswith('{') and value.endswith('}') ) or ( diff --git a/awx_collection/plugins/modules/tower_team.py b/awx_collection/plugins/modules/tower_team.py index bb8a89d570..e1506b2425 100644 --- a/awx_collection/plugins/modules/tower_team.py +++ b/awx_collection/plugins/modules/tower_team.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_team author: "Wayne Witzel III (@wwitzel3)" -version_added: "2.3" short_description: create, update, or destroy Ansible Tower team. description: - Create, update, or destroy Ansible Tower teams. See @@ -32,7 +31,6 @@ options: description: - To use when changing a team's name. type: str - version_added: "3.7" description: description: - The description to use for the team. diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py new file mode 100644 index 0000000000..165590520d --- /dev/null +++ b/awx_collection/plugins/modules/tower_token.py @@ -0,0 +1,201 @@ +#!/usr/bin/python +# coding: utf-8 -*- + + +# (c) 2020, John Westcott IV +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: tower_token +author: "John Westcott IV (@john-westcott-iv)" +version_added: "2.3" +short_description: create, update, or destroy Ansible Tower tokens. +description: + - Create or destroy Ansible Tower tokens. See + U(https://www.ansible.com/tower) for an overview. + - In addition, the module sets an Ansible fact which can be passed into other + tower_* modules as the parameter tower_oauthtoken. See examples for usage. + - Because of the sensitive nature of tokens, the created token value is only available once + through the Ansible fact. (See RETURN for details) + - Due to the nature of tokens in Tower this module is not idempotent. A second will + with the same parameters will create a new token. + - If you are creating a temporary token for use with modules you should delete the token + when you are done with it. See the example for how to do it. +options: + description: + description: + - Optional description of this access token. + required: False + type: str + default: '' + application: + description: + - The application tied to this token. + required: False + type: str + scope: + description: + - Allowed scopes, further restricts user's permissions. Must be a simple space-separated string with allowed scopes ['read', 'write']. + required: False + type: str + default: 'write' + choices: ["read", "write"] + existing_token: + description: The data structure produced from tower_token in create mode to be used with state absent. + type: dict + existing_token_id: + description: A token ID (number) which can be used to delete an arbitrary token with state absent. + type: str + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- block: + - name: Create a new token using an existing token + tower_token: + description: '{{ token_description }}' + scope: "write" + state: present + tower_oauthtoken: "{{ my_existing_token }}" + + - name: Delete this token + tower_token: + existing_token: "{{ tower_token }}" + state: absent + + - name: Create a new token using username/password + tower_token: + description: '{{ token_description }}' + scope: "write" + state: present + tower_username: "{{ my_username }}" + tower_password: "{{ my_password }}" + + - name: Use our new token to make another call + tower_job_list: + tower_oauthtoken: "{{ tower_token }}" + + always: + - name: Delete our Token with the token we created + tower_token: + existing_token: "{{ tower_token }}" + state: absent + when: tower_token is defined + +- name: Delete a token by its id + tower_token: + existing_token_id: 4 + state: absent +''' + +RETURN = ''' +tower_token: + type: dict + description: An Ansible Fact variable representing a Tower token object which can be used for auth in subsequent modules. See examples for usage. + contains: + token: + description: The token that was generated. This token can never be accessed again, make sure this value is noted before it is lost. + type: str + id: + description: The numeric ID of the token created + type: str + returned: on successful create +''' + +from ..module_utils.tower_api import TowerModule + + +def return_token(module, last_response): + # A token is special because you can never get the actual token ID back from the API. + # So the default module return would give you an ID but then the token would forever be masked on you. + # This method will return the entire token object we got back so that a user has access to the token + + module.json_output['ansible_facts'] = { + 'tower_token': last_response, + } + module.exit_json(**module.json_output) + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + description=dict(), + application=dict(), + scope=dict(choices=['read', 'write'], default='write'), + existing_token=dict(type='dict'), + existing_token_id=dict(), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ('existing_token', 'existing_token_id'), + ], + # If we are state absent make sure one of existing_token or existing_token_id are present + required_if=[ + ['state', 'absent', ('existing_token', 'existing_token_id'), True, ], + ], + ) + + # Extract our parameters + description = module.params.get('description') + application = module.params.get('application') + scope = module.params.get('scope') + existing_token = module.params.get('existing_token') + existing_token_id = module.params.get('existing_token_id') + state = module.params.get('state') + + if state == 'absent': + if not existing_token: + existing_token = module.get_one('tokens', **{ + 'data': { + 'id': existing_token_id, + } + }) + + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(existing_token) + + # Attempt to look up the related items the user specified (these will fail the module if not found) + application_id = None + if application: + application_id = module.resolve_name_to_id('applications', application) + + # Create the data that gets sent for create and update + new_fields = {} + if description is not None: + new_fields['description'] = description + if application is not None: + new_fields['application'] = application_id + if scope is not None: + new_fields['scope'] = scope + + # If the state was present and we can let the module build or update the existing item, this will return on its own + module.create_or_update_if_needed( + None, new_fields, + endpoint='tokens', item_type='token', + associations={ + }, + on_create=return_token, + ) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_user.py b/awx_collection/plugins/modules/tower_user.py index d710ed8a6d..7d049de016 100644 --- a/awx_collection/plugins/modules/tower_user.py +++ b/awx_collection/plugins/modules/tower_user.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_user author: "John Westcott IV (@john-westcott-iv)" -version_added: "2.3" short_description: create, update, or destroy Ansible Tower users. description: - Create, update, or destroy Ansible Tower users. See diff --git a/awx_collection/plugins/modules/tower_workflow_job_template.py b/awx_collection/plugins/modules/tower_workflow_job_template.py index 402933461e..c3ad692af4 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_workflow_job_template author: "John Westcott IV (@john-westcott-iv)" -version_added: "2.3" short_description: create, update, or destroy Ansible Tower workflow job templates. description: - Create, update, or destroy Ansible Tower workflow job templates. diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index 65603effae..629badce37 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -17,7 +17,6 @@ DOCUMENTATION = ''' --- module: tower_workflow_job_template_node author: "John Westcott IV (@john-westcott-iv)" -version_added: "2.3" short_description: create, update, or destroy Ansible Tower workflow job template nodes. description: - Create, update, or destroy Ansible Tower workflow job template nodes. diff --git a/awx_collection/plugins/modules/tower_workflow_launch.py b/awx_collection/plugins/modules/tower_workflow_launch.py index 3551f211b4..8ef73d82fc 100644 --- a/awx_collection/plugins/modules/tower_workflow_launch.py +++ b/awx_collection/plugins/modules/tower_workflow_launch.py @@ -14,7 +14,6 @@ DOCUMENTATION = ''' --- module: tower_workflow_launch author: "John Westcott IV (@john-westcott-iv)" -version_added: "2.8" short_description: Run a workflow in Ansible Tower description: - Launch an Ansible Tower workflows. See @@ -32,7 +31,6 @@ options: - Organization the workflow job template exists in. - Used to help lookup the object, cannot be modified using this module. - If not provided, will lookup by name only, which does not work with duplicates. - required: False type: str inventory: description: @@ -47,7 +45,6 @@ options: - A specific branch of the SCM project to run the template on. - This is only applicable if your project allows for branch override. type: str - version_added: "3.7" extra_vars: description: - Any extra vars required to launch the job. diff --git a/awx_collection/plugins/modules/tower_workflow_template.py b/awx_collection/plugins/modules/tower_workflow_template.py index a1accfb453..9a652a4373 100644 --- a/awx_collection/plugins/modules/tower_workflow_template.py +++ b/awx_collection/plugins/modules/tower_workflow_template.py @@ -17,11 +17,10 @@ DOCUMENTATION = ''' --- module: tower_workflow_template deprecated: - removed_in: "3.7" + removed_in: "14.0.0" why: Deprecated in favor of C(_workflow_job_template) and C(_workflow_job_template_node) modules. alternative: Use M(tower_workflow_job_template) and M(_workflow_job_template_node) instead. author: "Adrien Fleury (@fleu42)" -version_added: "2.7" short_description: create, update, or destroy Ansible Tower workflow template. description: - A tower-cli based module for CRUD actions on workflow job templates. @@ -37,12 +36,10 @@ options: description: - Prompt user for (extra_vars) on launch. type: bool - version_added: "2.9" ask_inventory: description: - Prompt user for inventory on launch. type: bool - version_added: "2.9" description: description: - The description to use for the workflow. @@ -54,7 +51,6 @@ options: inventory: description: - Name of the inventory to use for the job template. - version_added: "2.9" type: str name: description: @@ -153,7 +149,7 @@ def main(): "This module is replaced by the combination of tower_workflow_job_template and " "tower_workflow_job_template_node. This uses the old tower-cli and wll be " "removed in 2022." - ), version='4.2.0') + ), version='awx.awx:14.0.0') name = module.params.get('name') state = module.params.get('state') diff --git a/awx_collection/test/awx/test_module_utils.py b/awx_collection/test/awx/test_module_utils.py index 3c3cdf61c8..c7238d8c98 100644 --- a/awx_collection/test/awx/test_module_utils.py +++ b/awx_collection/test/awx/test_module_utils.py @@ -71,21 +71,14 @@ def test_duplicate_config(collection_import, silence_warning): 'tower_config_file': 'my_config' } - class DuplicateTestTowerModule(TowerModule): - def load_config(self, config_path): - assert config_path == 'my_config' - - def _load_params(self): - self.params = data - - cli_data = {'ANSIBLE_MODULE_ARGS': data} - testargs = ['module_file.py', json.dumps(cli_data)] - with mock.patch.object(sys, 'argv', testargs): + with mock.patch.object(TowerModule, 'load_config') as mock_load: argument_spec = dict( name=dict(required=True), zig=dict(type='str'), ) - DuplicateTestTowerModule(argument_spec=argument_spec) + TowerModule(argument_spec=argument_spec, direct_params=data) + assert mock_load.mock_calls[-1] == mock.call('my_config') + silence_warning.assert_called_once_with( 'The parameter(s) tower_username were provided at the same time as ' 'tower_config_file. Precedence may be unstable, ' diff --git a/awx_collection/test/awx/test_token.py b/awx_collection/test/awx/test_token.py new file mode 100644 index 0000000000..442fa2e9fb --- /dev/null +++ b/awx_collection/test/awx/test_token.py @@ -0,0 +1,29 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import OAuth2AccessToken + + +@pytest.mark.django_db +def test_create_token(run_module, admin_user): + + module_args = { + 'description': 'barfoo', + 'state': 'present', + 'scope': 'read', + 'tower_host': None, + 'tower_username': None, + 'tower_password': None, + 'validate_certs': None, + 'tower_oauthtoken': None, + 'tower_config_file': None, + } + + result = run_module('tower_token', module_args, admin_user) + assert result.get('changed'), result + + tokens = OAuth2AccessToken.objects.filter(description='barfoo') + assert len(tokens) == 1, 'Tokens with description of barfoo != 0: {0}'.format(len(tokens)) + assert tokens[0].scope == 'read', 'Token was not given read access' diff --git a/awx_collection/tests/integration/targets/tower_settings/tasks/main.yml b/awx_collection/tests/integration/targets/tower_settings/tasks/main.yml index a02ca673de..8a42f5768e 100644 --- a/awx_collection/tests/integration/targets/tower_settings/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_settings/tasks/main.yml @@ -74,3 +74,14 @@ - assert: that: - "result is changed" + +- name: Handle an omit value + tower_settings: + name: AWX_PROOT_BASE_PATH + value: '{{ junk_var | default(omit) }}' + register: result + ignore_errors: true + +- assert: + that: + - "'Unable to update settings' in result.msg" diff --git a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml new file mode 100644 index 0000000000..355d5dd02f --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml @@ -0,0 +1,110 @@ +--- +- name: Generate names + set_fact: + token_description: "AWX-Collection-tests-tower_token-description-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Try to use a token as a dict which is missing the token parameter + tower_job_list: + tower_oauthtoken: + not_token: "This has no token entry" + register: results + ignore_errors: true + +- assert: + that: + - results is failed + - '"The provided dict in tower_oauthtoken did not properly contain the token entry" == results.msg' + +- name: Try to use a token as a list + tower_job_list: + tower_oauthtoken: + - dummy_token + register: results + ignore_errors: true + +- assert: + that: + - results is failed + - '"The provided tower_oauthtoken type was not valid (list). Valid options are str or dict." == results.msg' + +- name: Try to delete a token with no existing_token or existing_token_id + tower_token: + state: absent + register: results + ignore_errors: true + +- assert: + that: + - results is failed + # We don't assert a message here because it handled by ansible + +- name: Try to delete a token with both existing_token or existing_token_id + tower_token: + existing_token: + id: 1234 + existing_token_id: 1234 + state: absent + register: results + ignore_errors: true + +- assert: + that: + - results is failed + # We don't assert a message here because it handled by ansible + + +- block: + - name: Create a Token + tower_token: + description: '{{ token_description }}' + scope: "write" + state: present + register: new_token + + - name: Validate our token works by token + tower_job_list: + tower_oauthtoken: "{{ tower_token.token }}" + register: job_list + + - name: Validate out token works by object + tower_job_list: + tower_oauthtoken: "{{ tower_token }}" + register: job_list + + always: + - name: Delete our Token with our own token + tower_token: + existing_token: "{{ tower_token }}" + tower_oauthtoken: "{{ tower_token }}" + state: absent + when: tower_token is defined + register: results + + - assert: + that: + - results is changed or results is skipped + +- block: + - name: Create a second token + tower_token: + description: '{{ token_description }}' + scope: "write" + state: present + register: results + + - assert: + that: + - results is changed + + always: + - name: Delete the second Token with our own token + tower_token: + existing_token_id: "{{ tower_token['id'] }}" + tower_oauthtoken: "{{ tower_token }}" + state: absent + when: tower_token is defined + register: results + + - assert: + that: + - results is changed or resuslts is skipped diff --git a/awx_collection/tests/sanity/ignore-2.10.txt b/awx_collection/tests/sanity/ignore-2.10.txt index 9242eefca9..76f35a9a85 100644 --- a/awx_collection/tests/sanity/ignore-2.10.txt +++ b/awx_collection/tests/sanity/ignore-2.10.txt @@ -1,6 +1,6 @@ plugins/modules/tower_receive.py validate-modules:deprecation-mismatch -plugins/modules/tower_receive.py validate-modules:invalid-documentation plugins/modules/tower_send.py validate-modules:deprecation-mismatch -plugins/modules/tower_send.py validate-modules:invalid-documentation plugins/modules/tower_workflow_template.py validate-modules:deprecation-mismatch -plugins/modules/tower_workflow_template.py validate-modules:invalid-documentation +plugins/modules/tower_credential.py pylint:wrong-collection-deprecated-version-tag +plugins/modules/tower_job_wait.py pylint:wrong-collection-deprecated-version-tag +plugins/modules/tower_notification.py pylint:wrong-collection-deprecated-version-tag diff --git a/awx_collection/tools/roles/generate/templates/tower_module.j2 b/awx_collection/tools/roles/generate/templates/tower_module.j2 index 5d687bfc24..a9834db28d 100644 --- a/awx_collection/tools/roles/generate/templates/tower_module.j2 +++ b/awx_collection/tools/roles/generate/templates/tower_module.j2 @@ -24,7 +24,7 @@ DOCUMENTATION = ''' --- module: tower_{{ singular_item_type }} author: "John Westcott IV (@john-westcott-iv)" -version_added: "2.3" +version_added: "4.0" short_description: create, update, or destroy Ansible Tower {{ human_readable }}. description: - Create, update, or destroy Ansible Tower {{ human_readable }}. See @@ -87,7 +87,6 @@ options: - The Tower OAuth token to use. required: False type: str - version_added: "3.7" extends_documentation_fragment: awx.awx.auth ''' diff --git a/awx_collection/tools/roles/template_galaxy/tasks/main.yml b/awx_collection/tools/roles/template_galaxy/tasks/main.yml index 4f018d793b..4edd056f43 100644 --- a/awx_collection/tools/roles/template_galaxy/tasks/main.yml +++ b/awx_collection/tools/roles/template_galaxy/tasks/main.yml @@ -30,6 +30,13 @@ path: "{{ collection_path }}/plugins/inventory/tower.py" regexp: "^ NAME = 'awx.awx.tower' # REPLACE$" replace: " NAME = '{{ collection_namespace }}.{{ collection_package }}.tower' # REPLACE" + + - name: Get sanity tests to work with non-default name + lineinfile: + path: "{{ collection_path }}/tests/sanity/ignore-2.10.txt" + state: absent + regexp: ' pylint:wrong-collection-deprecated-version-tag$' + when: - (collection_package != 'awx') or (collection_namespace != 'awx') diff --git a/docs/licenses/amqp-2.5.2.tar.gz b/docs/licenses/amqp-2.5.2.tar.gz deleted file mode 100644 index 9ee574fa18..0000000000 Binary files a/docs/licenses/amqp-2.5.2.tar.gz and /dev/null differ diff --git a/docs/licenses/baron-0.6.6.tar.gz b/docs/licenses/baron-0.6.6.tar.gz deleted file mode 100644 index 524001e6ea..0000000000 Binary files a/docs/licenses/baron-0.6.6.tar.gz and /dev/null differ diff --git a/docs/licenses/redbaron-0.6.3.tar.gz b/docs/licenses/redbaron-0.6.3.tar.gz deleted file mode 100644 index 31611542e7..0000000000 Binary files a/docs/licenses/redbaron-0.6.3.tar.gz and /dev/null differ diff --git a/docs/licenses/suds-0.4.tar.gz b/docs/licenses/suds-0.4.tar.gz deleted file mode 100644 index 168d3673e9..0000000000 Binary files a/docs/licenses/suds-0.4.tar.gz and /dev/null differ diff --git a/installer/roles/image_build/templates/Dockerfile.j2 b/installer/roles/image_build/templates/Dockerfile.j2 index 80cf4985e4..dcab14bed9 100644 --- a/installer/roles/image_build/templates/Dockerfile.j2 +++ b/installer/roles/image_build/templates/Dockerfile.j2 @@ -220,7 +220,6 @@ RUN for dir in \ /vendor ; \ do mkdir -m 0775 -p $dir ; chmod g+rw $dir ; chgrp root $dir ; done && \ for file in \ - /etc/supervisord.conf \ /var/run/nginx.pid \ /venv/awx/lib/python3.6/site-packages/awx.egg-link ; \ do touch $file ; chmod g+rw $file ; done diff --git a/installer/roles/image_build/templates/launch_awx_task.sh.j2 b/installer/roles/image_build/templates/launch_awx_task.sh.j2 index edaf3bf362..dd54af5b3e 100755 --- a/installer/roles/image_build/templates/launch_awx_task.sh.j2 +++ b/installer/roles/image_build/templates/launch_awx_task.sh.j2 @@ -12,6 +12,8 @@ ANSIBLE_REMOTE_TEMP=/tmp ANSIBLE_LOCAL_TEMP=/tmp ansible -i "127.0.0.1," -c loca if [ -z "$AWX_SKIP_MIGRATIONS" ]; then awx-manage migrate --noinput + awx-manage provision_instance --hostname=$(hostname) + awx-manage register_queue --queuename=tower --instance_percent=100 fi if [ ! -z "$AWX_ADMIN_USER" ]&&[ ! -z "$AWX_ADMIN_PASSWORD" ]; then @@ -21,8 +23,6 @@ if [ ! -z "$AWX_ADMIN_USER" ]&&[ ! -z "$AWX_ADMIN_PASSWORD" ]; then {% endif %} fi echo 'from django.conf import settings; x = settings.AWX_TASK_ENV; x["HOME"] = "/var/lib/awx"; settings.AWX_TASK_ENV = x' | awx-manage shell -awx-manage provision_instance --hostname=$(hostname) -awx-manage register_queue --queuename=tower --instance_percent=100 unset $(cut -d = -f -1 /etc/tower/conf.d/environment.sh) diff --git a/requirements/collections_requirements.yml b/requirements/collections_requirements.yml index 35ed989908..15b499d361 100644 --- a/requirements/collections_requirements.yml +++ b/requirements/collections_requirements.yml @@ -7,7 +7,7 @@ collections: - name: amazon.aws version: 0.1.1 # version 0.1.0 seems to have gone missing - name: theforeman.foreman - version: 0.8.0 + version: 0.8.1 - name: google.cloud version: 0.0.9 # contains PR 167, should be good to go - name: openstack.cloud diff --git a/tools/docker-compose-cluster.yml b/tools/docker-compose-cluster.yml index 4dc7ef49ce..95f7f5aaa8 100644 --- a/tools/docker-compose-cluster.yml +++ b/tools/docker-compose-cluster.yml @@ -31,6 +31,7 @@ services: - "../:/awx_devel" - "./redis/redis_socket_ha_1:/var/run/redis/" - "./memcached/:/var/run/memcached" + - "./docker-compose/supervisor.conf:/etc/supervisord.conf" ports: - "5899-5999:5899-5999" awx-2: @@ -50,6 +51,7 @@ services: - "../:/awx_devel" - "./redis/redis_socket_ha_2:/var/run/redis/" - "./memcached/:/var/run/memcached" + - "./docker-compose/supervisor.conf:/etc/supervisord.conf" ports: - "7899-7999:7899-7999" awx-3: @@ -69,6 +71,7 @@ services: - "../:/awx_devel" - "./redis/redis_socket_ha_3:/var/run/redis/" - "./memcached/:/var/run/memcached" + - "./docker-compose/supervisor.conf:/etc/supervisord.conf" ports: - "8899-8999:8899-8999" redis_1: diff --git a/tools/docker-compose.yml b/tools/docker-compose.yml index a96c835c02..b1e8fb93c7 100644 --- a/tools/docker-compose.yml +++ b/tools/docker-compose.yml @@ -34,7 +34,7 @@ services: - "../awx/projects/:/var/lib/awx/projects/" - "./redis/redis_socket_standalone:/var/run/redis/" - "./memcached/:/var/run/memcached" - - "./rsyslog/:/var/lib/awx/rsyslog" + - "./docker-compose/supervisor.conf:/etc/supervisord.conf" privileged: true tty: true # A useful container that simply passes through log messages to the console diff --git a/tools/docker-compose/bootstrap_development.sh b/tools/docker-compose/bootstrap_development.sh index 00642d5528..095d3e0d04 100755 --- a/tools/docker-compose/bootstrap_development.sh +++ b/tools/docker-compose/bootstrap_development.sh @@ -20,7 +20,6 @@ else fi make awx-link -yes | cp -rf /awx_devel/tools/docker-compose/supervisor.conf /etc/supervisord.conf # AWX bootstrapping make version_file