Merge pull request #1 from ansible/devel

Merging remote to devel
This commit is contained in:
Sean Sullivan
2020-06-09 07:50:36 -05:00
committed by GitHub
90 changed files with 931 additions and 361 deletions

View File

@@ -50,9 +50,9 @@ conjur_inputs = {
def conjur_backend(**kwargs): def conjur_backend(**kwargs):
url = kwargs['url'] url = kwargs['url']
api_key = kwargs['api_key'] api_key = kwargs['api_key']
account = quote(kwargs['account']) account = quote(kwargs['account'], safe='')
username = quote(kwargs['username']) username = quote(kwargs['username'], safe='')
secret_path = quote(kwargs['secret_path']) secret_path = quote(kwargs['secret_path'], safe='')
version = kwargs.get('secret_version') version = kwargs.get('secret_version')
cacert = kwargs.get('cacert', None) cacert = kwargs.get('cacert', None)

View File

@@ -35,6 +35,7 @@ class WorkerSignalHandler:
def __init__(self): def __init__(self):
self.kill_now = False self.kill_now = False
signal.signal(signal.SIGTERM, signal.SIG_DFL)
signal.signal(signal.SIGINT, self.exit_gracefully) signal.signal(signal.SIGINT, self.exit_gracefully)
def exit_gracefully(self, *args, **kwargs): def exit_gracefully(self, *args, **kwargs):

View File

@@ -16,31 +16,24 @@ class InstanceNotFound(Exception):
super(InstanceNotFound, self).__init__(*args, **kwargs) 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): def get_create_update_instance_group(self):
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):
created = False created = False
changed = False changed = False
(ig, created) = InstanceGroup.objects.get_or_create(name=self.queuename)
(ig, created) = InstanceGroup.objects.get_or_create(name=queuename) if ig.policy_instance_percentage != self.instance_percent:
if ig.policy_instance_percentage != instance_percent: ig.policy_instance_percentage = self.instance_percent
ig.policy_instance_percentage = instance_percent
changed = True changed = True
if ig.policy_instance_minimum != instance_min: if ig.policy_instance_minimum != self.instance_min:
ig.policy_instance_minimum = instance_min ig.policy_instance_minimum = self.instance_min
changed = True changed = True
if changed: if changed:
@@ -48,12 +41,12 @@ class Command(BaseCommand):
return (ig, created, changed) return (ig, created, changed)
def update_instance_group_controller(self, ig, controller): def update_instance_group_controller(self, ig):
changed = False changed = False
control_ig = None control_ig = None
if controller: if self.controller:
control_ig = InstanceGroup.objects.filter(name=controller).first() control_ig = InstanceGroup.objects.filter(name=self.controller).first()
if control_ig and ig.controller_id != control_ig.pk: if control_ig and ig.controller_id != control_ig.pk:
ig.controller = control_ig ig.controller = control_ig
@@ -62,10 +55,10 @@ class Command(BaseCommand):
return (control_ig, changed) return (control_ig, changed)
def add_instances_to_group(self, ig, hostname_list): def add_instances_to_group(self, ig):
changed = False 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 = [] instances = []
for inst_name in instance_list_unique: for inst_name in instance_list_unique:
instance = Instance.objects.filter(hostname=inst_name) instance = Instance.objects.filter(hostname=inst_name)
@@ -86,43 +79,61 @@ class Command(BaseCommand):
return (instances, changed) return (instances, changed)
def handle(self, **options): def register(self):
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(",")
with advisory_lock('cluster_policy_lock'): with advisory_lock('cluster_policy_lock'):
with transaction.atomic(): with transaction.atomic():
changed2 = False changed2 = False
changed3 = 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: if created:
print("Creating instance group {}".format(ig.name)) print("Creating instance group {}".format(ig.name))
elif not created: elif not created:
print("Instance Group already registered {}".format(ig.name)) print("Instance Group already registered {}".format(ig.name))
if ctrl: if self.controller:
(ig_ctrl, changed2) = self.update_instance_group_controller(ig, ctrl) (ig_ctrl, changed2) = self.update_instance_group_controller(ig)
if changed2: if changed2:
print("Set controller group {} on {}.".format(ctrl, queuename)) print("Set controller group {} on {}.".format(self.controller, self.queuename))
try: try:
(instances, changed3) = self.add_instances_to_group(ig, hostname_list) (instances, changed3) = self.add_instances_to_group(ig)
for i in instances: for i in instances:
print("Added instance {} to {}".format(i.hostname, ig.name)) print("Added instance {} to {}".format(i.hostname, ig.name))
except InstanceNotFound as e: except InstanceNotFound as e:
instance_not_found_err = e self.instance_not_found_err = e
if any([changed1, changed2, changed3]): if any([changed1, changed2, changed3]):
print('(changed: True)') 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) sys.exit(1)

View File

@@ -149,8 +149,11 @@ class InstanceManager(models.Manager):
def get_or_register(self): def get_or_register(self):
if settings.AWX_AUTO_DEPROVISION_INSTANCES: if settings.AWX_AUTO_DEPROVISION_INSTANCES:
from awx.main.management.commands.register_queue import RegisterQueue
pod_ip = os.environ.get('MY_POD_IP') 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: else:
return (False, self.me()) return (False, self.me())

View File

@@ -3,7 +3,6 @@
import logging import logging
import requests import requests
import json
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@@ -45,7 +44,7 @@ class MattermostBackend(AWXBaseEmailBackend, CustomNotificationBase):
payload['text'] = m.subject payload['text'] = m.subject
r = requests.post("{}".format(m.recipients()[0]), 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: if r.status_code >= 400:
logger.error(smart_text(_("Error sending notification mattermost: {}").format(r.text))) logger.error(smart_text(_("Error sending notification mattermost: {}").format(r.text)))
if not self.fail_silently: if not self.fail_silently:

View File

@@ -581,3 +581,4 @@ class TaskManager():
logger.debug("Starting Scheduler") logger.debug("Starting Scheduler")
with task_manager_bulk_reschedule(): with task_manager_bulk_reschedule():
self._schedule() self._schedule()
logger.debug("Finishing Scheduler")

View File

@@ -13,6 +13,10 @@ class RSysLogHandler(logging.handlers.SysLogHandler):
append_nul = False append_nul = False
def _connect_unixsocket(self, address):
super(RSysLogHandler, self)._connect_unixsocket(address)
self.socket.setblocking(False)
def emit(self, msg): def emit(self, msg):
if not settings.LOG_AGGREGATOR_ENABLED: if not settings.LOG_AGGREGATOR_ENABLED:
return return
@@ -26,6 +30,14 @@ class RSysLogHandler(logging.handlers.SysLogHandler):
# unfortunately, we can't log that because...rsyslogd is down (and # unfortunately, we can't log that because...rsyslogd is down (and
# would just us back ddown this code path) # would just us back ddown this code path)
pass pass
except BlockingIOError:
# for <some reason>, 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 ColorHandler = logging.StreamHandler

View File

@@ -740,7 +740,9 @@ class SAMLOrgAttrField(HybridDictField):
class SAMLTeamAttrTeamOrgMapField(HybridDictField): class SAMLTeamAttrTeamOrgMapField(HybridDictField):
team = fields.CharField(required=True, allow_null=False) 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 = fields.CharField(required=True, allow_null=False)
organization_alias = fields.CharField(required=False, allow_null=True)
child = _Forbidden() child = _Forbidden()

View File

@@ -187,13 +187,22 @@ def update_user_teams_by_saml_attr(backend, details, user=None, *args, **kwargs)
team_ids = [] team_ids = []
for team_name_map in team_map.get('team_org_map', []): 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 team_name in saml_team_names:
if not team_name_map.get('organization', ''): if not organization_name:
# Settings field validation should prevent this. # Settings field validation should prevent this.
logger.error("organization name invalid for team {}".format(team_name)) logger.error("organization name invalid for team {}".format(team_name))
continue 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 = Team.objects.get_or_create(name=team_name, organization=org)[0]
team_ids.append(team.id) team_ids.append(team.id)

View File

@@ -193,6 +193,10 @@ class TestSAMLAttr():
{'team': 'Red', 'organization': 'Default1'}, {'team': 'Red', 'organization': 'Default1'},
{'team': 'Green', 'organization': 'Default1'}, {'team': 'Green', 'organization': 'Default1'},
{'team': 'Green', 'organization': 'Default3'}, {'team': 'Green', 'organization': 'Default3'},
{
'team': 'Yellow', 'team_alias': 'Yellow_Alias',
'organization': 'Default4', 'organization_alias': 'Default4_Alias'
},
] ]
} }
return MockSettings() 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='Default1').member_role.members.count() == 3
assert Team.objects.get(name='Green', organization__name='Default3').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

View File

@@ -71,6 +71,14 @@ class TestSAMLTeamAttrField():
{'team': 'Engineering', 'organization': 'Ansible2'}, {'team': 'Engineering', 'organization': 'Ansible2'},
{'team': 'Engineering2', 'organization': 'Ansible'}, {'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): def test_internal_value_valid(self, data):
field = SAMLTeamAttrField() field = SAMLTeamAttrField()

View File

@@ -14435,9 +14435,9 @@
} }
}, },
"websocket-extensions": { "websocket-extensions": {
"version": "0.1.3", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
"integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
"dev": true "dev": true
}, },
"whet.extend": { "whet.extend": {

View File

@@ -16320,9 +16320,9 @@
} }
}, },
"websocket-extensions": { "websocket-extensions": {
"version": "0.1.3", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
"integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==" "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="
}, },
"whatwg-encoding": { "whatwg-encoding": {
"version": "1.0.5", "version": "1.0.5",

View File

@@ -55,12 +55,12 @@ function App() {
{getRouteConfig(i18n) {getRouteConfig(i18n)
.flatMap(({ routes }) => routes) .flatMap(({ routes }) => routes)
.map(({ path, screen: Screen }) => ( .map(({ path, screen: Screen }) => (
<ProtectedRoute auth key={path} path={path}> <ProtectedRoute key={path} path={path}>
<Screen match={match} /> <Screen match={match} />
</ProtectedRoute> </ProtectedRoute>
)) ))
.concat( .concat(
<ProtectedRoute auth key="not-found" path="*"> <ProtectedRoute key="not-found" path="*">
<NotFound /> <NotFound />
</ProtectedRoute> </ProtectedRoute>
)} )}

View File

@@ -40,7 +40,13 @@ class ClipboardCopyButton extends React.Component {
}; };
render() { render() {
const { clickTip, entryDelay, exitDelay, hoverTip } = this.props; const {
copyTip,
entryDelay,
exitDelay,
copiedSuccessTip,
isDisabled,
} = this.props;
const { copied } = this.state; const { copied } = this.state;
return ( return (
@@ -48,12 +54,13 @@ class ClipboardCopyButton extends React.Component {
entryDelay={entryDelay} entryDelay={entryDelay}
exitDelay={exitDelay} exitDelay={exitDelay}
trigger="mouseenter focus click" trigger="mouseenter focus click"
content={copied ? clickTip : hoverTip} content={copied ? copiedSuccessTip : copyTip}
> >
<Button <Button
isDisabled={isDisabled}
variant="plain" variant="plain"
onClick={this.handleCopyClick} onClick={this.handleCopyClick}
aria-label={hoverTip} aria-label={copyTip}
> >
<CopyIcon /> <CopyIcon />
</Button> </Button>
@@ -63,12 +70,13 @@ class ClipboardCopyButton extends React.Component {
} }
ClipboardCopyButton.propTypes = { ClipboardCopyButton.propTypes = {
clickTip: PropTypes.string.isRequired, copyTip: PropTypes.string.isRequired,
entryDelay: PropTypes.number, entryDelay: PropTypes.number,
exitDelay: PropTypes.number, exitDelay: PropTypes.number,
hoverTip: PropTypes.string.isRequired, copiedSuccessTip: PropTypes.string.isRequired,
stringToCopy: PropTypes.string.isRequired, stringToCopy: PropTypes.string.isRequired,
switchDelay: PropTypes.number, switchDelay: PropTypes.number,
isDisabled: PropTypes.bool.isRequired,
}; };
ClipboardCopyButton.defaultProps = { ClipboardCopyButton.defaultProps = {

View File

@@ -13,6 +13,7 @@ describe('ClipboardCopyButton', () => {
clickTip="foo" clickTip="foo"
hoverTip="bar" hoverTip="bar"
stringToCopy="foobar!" stringToCopy="foobar!"
isDisabled={false}
/> />
); );
expect(wrapper).toHaveLength(1); expect(wrapper).toHaveLength(1);
@@ -23,6 +24,7 @@ describe('ClipboardCopyButton', () => {
clickTip="foo" clickTip="foo"
hoverTip="bar" hoverTip="bar"
stringToCopy="foobar!" stringToCopy="foobar!"
isDisabled={false}
/> />
).find('ClipboardCopyButton'); ).find('ClipboardCopyButton');
expect(wrapper.state('copied')).toBe(false); expect(wrapper.state('copied')).toBe(false);
@@ -33,4 +35,15 @@ describe('ClipboardCopyButton', () => {
wrapper.update(); wrapper.update();
expect(wrapper.state('copied')).toBe(false); expect(wrapper.state('copied')).toBe(false);
}); });
test('should render disabled button', () => {
const wrapper = mountWithContexts(
<ClipboardCopyButton
clickTip="foo"
hoverTip="bar"
stringToCopy="foobar!"
isDisabled
/>
);
expect(wrapper.find('Button').prop('isDisabled')).toBe(true);
});
}); });

View File

@@ -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 { useFormikContext } from 'formik';
import { withI18n } from '@lingui/react';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import PromptDetail from '../../PromptDetail';
import mergeExtraVars, { maskPasswords } from '../mergeExtraVars'; import mergeExtraVars, { maskPasswords } from '../mergeExtraVars';
import getSurveyValues from '../getSurveyValues'; 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 { values } = useFormikContext();
const surveyValues = getSurveyValues(values); const surveyValues = getSurveyValues(values);
@@ -29,21 +47,26 @@ function PreviewStep({ resource, config, survey, formErrors }) {
} }
return ( return (
<> <Fragment>
{formErrors.length > 0 && (
<ErrorMessageWrapper>
{i18n._(t`Some of the previous step(s) have errors`)}
<Tooltip
position="right"
content={i18n._(t`See errors on the left`)}
trigger="click mouseenter focus"
>
<ExclamationCircleIcon />
</Tooltip>
</ErrorMessageWrapper>
)}
<PromptDetail <PromptDetail
resource={resource} resource={resource}
launchConfig={config} launchConfig={config}
overrides={overrides} overrides={overrides}
/> />
{formErrors && ( </Fragment>
<ul css="color: red">
{Object.keys(formErrors).map(
field => `${field}: ${formErrors[field]}`
)}
</ul>
)}
</>
); );
} }
export default PreviewStep; export default withI18n()(PreviewStep);

View File

@@ -24,6 +24,10 @@ const survey = {
], ],
}; };
const formErrors = {
inventory: 'An inventory must be selected',
};
describe('PreviewStep', () => { describe('PreviewStep', () => {
test('should render PromptDetail', async () => { test('should render PromptDetail', async () => {
let wrapper; let wrapper;
@@ -37,6 +41,7 @@ describe('PreviewStep', () => {
survey_enabled: true, survey_enabled: true,
}} }}
survey={survey} survey={survey}
formErrors={formErrors}
/> />
</Formik> </Formik>
); );
@@ -62,6 +67,7 @@ describe('PreviewStep', () => {
config={{ config={{
ask_limit_on_launch: true, ask_limit_on_launch: true,
}} }}
formErrors={formErrors}
/> />
</Formik> </Formik>
); );
@@ -85,6 +91,7 @@ describe('PreviewStep', () => {
config={{ config={{
ask_variables_on_launch: true, ask_variables_on_launch: true,
}} }}
formErrors={formErrors}
/> />
</Formik> </Formik>
); );

View File

@@ -19,7 +19,8 @@ export default function useCredentialsStep(
initialValues: getInitialValues(config, resource), initialValues: getInitialValues(config, resource),
validate, validate,
isReady: true, isReady: true,
error: null, contentError: null,
formError: null,
setTouched: setFieldsTouched => { setTouched: setFieldsTouched => {
setFieldsTouched({ setFieldsTouched({
credentials: true, credentials: true,

View File

@@ -27,7 +27,8 @@ export default function useInventoryStep(config, resource, visitedSteps, i18n) {
initialValues: getInitialValues(config, resource), initialValues: getInitialValues(config, resource),
validate, validate,
isReady: true, isReady: true,
error: null, contentError: null,
formError: stepErrors,
setTouched: setFieldsTouched => { setTouched: setFieldsTouched => {
setFieldsTouched({ setFieldsTouched({
inventory: true, inventory: true,

View File

@@ -24,7 +24,8 @@ export default function useOtherPrompt(config, resource, visitedSteps, i18n) {
initialValues: getInitialValues(config, resource), initialValues: getInitialValues(config, resource),
validate, validate,
isReady: true, isReady: true,
error: null, contentError: null,
formError: stepErrors,
setTouched: setFieldsTouched => { setTouched: setFieldsTouched => {
setFieldsTouched({ setFieldsTouched({
job_type: true, job_type: true,

View File

@@ -54,7 +54,8 @@ export default function useSurveyStep(config, resource, visitedSteps, i18n) {
validate, validate,
survey, survey,
isReady: !isLoading && !!survey, isReady: !isLoading && !!survey,
error, contentError: error,
formError: stepErrors,
setTouched: setFieldsTouched => { setTouched: setFieldsTouched => {
if (!survey || !survey.spec) { if (!survey || !survey.spec) {
return; return;

View File

@@ -13,14 +13,13 @@ export default function useSteps(config, resource, i18n) {
useOtherPromptsStep(config, resource, visited, i18n), useOtherPromptsStep(config, resource, visited, i18n),
useSurveyStep(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( steps.push(
usePreviewStep( usePreviewStep(config, resource, steps[3].survey, formErrorsContent, i18n)
config,
resource,
steps[3].survey,
{}, // TODO: formErrors ?
i18n
)
); );
const pfSteps = steps.map(s => s.step).filter(s => s != null); 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 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 validate = values => {
const errors = steps.reduce((acc, cur) => { const errors = steps.reduce((acc, cur) => {

View File

@@ -138,7 +138,7 @@ function getRouteConfig(i18n) {
screen: InstanceGroups, screen: InstanceGroups,
}, },
{ {
title: i18n._(t`Integrations`), title: i18n._(t`Applications`),
path: '/applications', path: '/applications',
screen: Applications, screen: Applications,
}, },

View File

@@ -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 (
<>
<Switch>
<Redirect
from="/applications/:id"
to="/applications/:id/details"
exact
/>
<Route path="/applications/:id/edit">
<ApplicationEdit />
</Route>
<Route path="/applications/:id/details">
<ApplicationDetails />
</Route>
</Switch>
</>
);
}
export default Application;

View File

@@ -0,0 +1 @@
export { default } from './Application';

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { Card, PageSection } from '@patternfly/react-core';
function ApplicatonAdd() {
return (
<>
<PageSection>
<Card>
<div>Applications Add</div>
</Card>
</PageSection>
</>
);
}
export default ApplicatonAdd;

View File

@@ -0,0 +1 @@
export { default } from './ApplicationAdd';

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { Card, PageSection } from '@patternfly/react-core';
function ApplicationDetails() {
return (
<PageSection>
<Card>Application Details</Card>
</PageSection>
);
}
export default ApplicationDetails;

View File

@@ -0,0 +1 @@
export { default } from './ApplicationDetails';

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { Card, PageSection } from '@patternfly/react-core';
function ApplicationEdit() {
return (
<PageSection>
<Card>Application Edit</Card>
</PageSection>
);
}
export default ApplicationEdit;

View File

@@ -0,0 +1 @@
export { default } from './ApplicationEdit';

View File

@@ -1,26 +1,49 @@
import React, { Component, Fragment } from 'react'; import React, { useState, useCallback } from 'react';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import { Route, Switch } from 'react-router-dom';
PageSection,
PageSectionVariants,
Title,
} from '@patternfly/react-core';
class Applications extends Component { import ApplicationsList from './ApplicationsList';
render() { import ApplicationAdd from './ApplicationAdd';
const { i18n } = this.props; import Application from './Application';
const { light } = PageSectionVariants; import Breadcrumbs from '../../components/Breadcrumbs';
return ( function Applications({ i18n }) {
<Fragment> const [breadcrumbConfig, setBreadcrumbConfig] = useState({
<PageSection variant={light} className="pf-m-condensed"> '/applications': i18n._(t`Applications`),
<Title size="2xl">{i18n._(t`Applications`)}</Title> '/applications/add': i18n._(t`Create New Application`),
</PageSection> });
<PageSection />
</Fragment> 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 (
<>
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
<Switch>
<Route path="/applications/add">
<ApplicationAdd />
</Route>
<Route path="/applications/:id">
<Application setBreadcrumb={buildBreadcrumbConfig} />
</Route>
<Route path="/applications">
<ApplicationsList />
</Route>
</Switch>
</>
);
} }
export default withI18n()(Applications); export default withI18n()(Applications);

View File

@@ -7,12 +7,10 @@ import Applications from './Applications';
describe('<Applications />', () => { describe('<Applications />', () => {
let pageWrapper; let pageWrapper;
let pageSections; let pageSections;
let title;
beforeEach(() => { beforeEach(() => {
pageWrapper = mountWithContexts(<Applications />); pageWrapper = mountWithContexts(<Applications />);
pageSections = pageWrapper.find('PageSection'); pageSections = pageWrapper.find('PageSection');
title = pageWrapper.find('Title');
}); });
afterEach(() => { afterEach(() => {
@@ -21,9 +19,7 @@ describe('<Applications />', () => {
test('initially renders without crashing', () => { test('initially renders without crashing', () => {
expect(pageWrapper.length).toBe(1); expect(pageWrapper.length).toBe(1);
expect(pageSections.length).toBe(2); expect(pageSections.length).toBe(1);
expect(title.length).toBe(1);
expect(title.props().size).toBe('2xl');
expect(pageSections.first().props().variant).toBe('light'); expect(pageSections.first().props().variant).toBe('light');
}); });
}); });

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { Card, PageSection } from '@patternfly/react-core';
function ApplicationsList() {
return (
<>
<PageSection>
<Card>
<div>Applications List</div>
</Card>
</PageSection>
</>
);
}
export default ApplicationsList;

View File

@@ -0,0 +1 @@
export { default } from './ApplicationsList';

View File

@@ -32,6 +32,11 @@ const DataListAction = styled(_DataListAction)`
grid-gap: 16px; grid-gap: 16px;
grid-template-columns: repeat(3, 40px); grid-template-columns: repeat(3, 40px);
`; `;
const Label = styled.span`
color: var(--pf-global--disabled-color--100);
`;
function ProjectListItem({ function ProjectListItem({
project, project,
isSelected, isSelected,
@@ -121,13 +126,17 @@ function ProjectListItem({
</DataListCell>, </DataListCell>,
<DataListCell key="revision"> <DataListCell key="revision">
{project.scm_revision.substring(0, 7)} {project.scm_revision.substring(0, 7)}
{project.scm_revision ? ( {!project.scm_revision && (
<ClipboardCopyButton <Label aria-label={i18n._(t`copy to clipboard disabled`)}>
stringToCopy={project.scm_revision} {i18n._(t`Sync for revision`)}
hoverTip={i18n._(t`Copy full revision to clipboard.`)} </Label>
clickTip={i18n._(t`Successfully copied to clipboard!`)} )}
/> <ClipboardCopyButton
) : null} isDisabled={!project.scm_revision}
stringToCopy={project.scm_revision}
copyTip={i18n._(t`Copy full revision to clipboard.`)}
copiedSuccessTip={i18n._(t`Successfully copied to clipboard!`)}
/>
</DataListCell>, </DataListCell>,
]} ]}
/> />

View File

@@ -218,4 +218,34 @@ describe('<ProjectsListItem />', () => {
); );
expect(wrapper.find('CopyButton').length).toBe(0); expect(wrapper.find('CopyButton').length).toBe(0);
}); });
test('should render disabled copy to clipboard button', () => {
const wrapper = mountWithContexts(
<ProjectsListItem
isSelected={false}
detailUrl="/project/1"
onSelect={() => {}}
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);
});
}); });

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { withFormik, useField, useFormikContext } from 'formik'; import { withFormik, useField } from 'formik';
import { import {
Form, Form,
FormGroup, FormGroup,
@@ -52,8 +52,6 @@ function JobTemplateForm({
submitError, submitError,
i18n, i18n,
}) { }) {
const { values: formikValues } = useFormikContext();
const [contentError, setContentError] = useState(false); const [contentError, setContentError] = useState(false);
const [inventory, setInventory] = useState( const [inventory, setInventory] = useState(
template?.summary_fields?.inventory template?.summary_fields?.inventory
@@ -65,6 +63,7 @@ function JobTemplateForm({
Boolean(template.webhook_service) Boolean(template.webhook_service)
); );
const [askInventoryOnLaunchField] = useField('ask_inventory_on_launch');
const [jobTypeField, jobTypeMeta, jobTypeHelpers] = useField({ const [jobTypeField, jobTypeMeta, jobTypeHelpers] = useField({
name: 'job_type', name: 'job_type',
validate: required(null, i18n), validate: required(null, i18n),
@@ -81,7 +80,7 @@ function JobTemplateForm({
}); });
const [credentialField, , credentialHelpers] = useField('credentials'); const [credentialField, , credentialHelpers] = useField('credentials');
const [labelsField, , labelsHelpers] = useField('labels'); const [labelsField, , labelsHelpers] = useField('labels');
const [limitField, limitMeta] = useField('limit'); const [limitField, limitMeta, limitHelpers] = useField('limit');
const [verbosityField] = useField('verbosity'); const [verbosityField] = useField('verbosity');
const [diffModeField, , diffModeHelpers] = useField('diff_mode'); const [diffModeField, , diffModeHelpers] = useField('diff_mode');
const [instanceGroupsField, , instanceGroupsHelpers] = useField( const [instanceGroupsField, , instanceGroupsHelpers] = useField(
@@ -231,7 +230,7 @@ function JobTemplateForm({
</FieldWithPrompt> </FieldWithPrompt>
<FieldWithPrompt <FieldWithPrompt
fieldId="template-inventory" fieldId="template-inventory"
isRequired={!formikValues.ask_inventory_on_launch} isRequired={!askInventoryOnLaunchField.value}
label={i18n._(t`Inventory`)} label={i18n._(t`Inventory`)}
promptId="template-ask-inventory-on-launch" promptId="template-ask-inventory-on-launch"
promptName="ask_inventory_on_launch" promptName="ask_inventory_on_launch"
@@ -245,11 +244,11 @@ function JobTemplateForm({
inventoryHelpers.setValue(value ? value.id : null); inventoryHelpers.setValue(value ? value.id : null);
setInventory(value); setInventory(value);
}} }}
required={!formikValues.ask_inventory_on_launch} required={!askInventoryOnLaunchField.value}
touched={inventoryMeta.touched} touched={inventoryMeta.touched}
error={inventoryMeta.error} error={inventoryMeta.error}
/> />
{(inventoryMeta.touched || formikValues.ask_inventory_on_launch) && {(inventoryMeta.touched || askInventoryOnLaunchField.value) &&
inventoryMeta.error && ( inventoryMeta.error && (
<div <div
className="pf-c-form__helper-text pf-m-error" className="pf-c-form__helper-text pf-m-error"
@@ -283,8 +282,8 @@ function JobTemplateForm({
<TextInput <TextInput
id="template-scm-branch" id="template-scm-branch"
{...scmField} {...scmField}
onChange={(value, event) => { onChange={value => {
scmField.onChange(event); scmHelpers.setValue(value);
}} }}
/> />
</FieldWithPrompt> </FieldWithPrompt>
@@ -383,8 +382,8 @@ function JobTemplateForm({
id="template-limit" id="template-limit"
{...limitField} {...limitField}
isValid={!limitMeta.touched || !limitMeta.error} isValid={!limitMeta.touched || !limitMeta.error}
onChange={(value, event) => { onChange={value => {
limitField.onChange(event); limitHelpers.setValue(value);
}} }}
/> />
</FieldWithPrompt> </FieldWithPrompt>

View File

@@ -29,6 +29,7 @@ describe('<JobTemplateForm />', () => {
playbook: 'Baz', playbook: 'Baz',
type: 'job_template', type: 'job_template',
scm_branch: 'Foo', scm_branch: 'Foo',
limit: '5000',
summary_fields: { summary_fields: {
inventory: { inventory: {
id: 2, id: 2,
@@ -184,9 +185,10 @@ describe('<JobTemplateForm />', () => {
wrapper.update(); wrapper.update();
await act(async () => { await act(async () => {
wrapper.find('input#template-scm-branch').simulate('change', { wrapper.find('TextInputBase#template-scm-branch').prop('onChange')(
target: { value: 'devel', name: 'scm_branch' }, 'devel'
}); );
wrapper.find('TextInputBase#template-limit').prop('onChange')(1234567890);
wrapper.find('AnsibleSelect[name="playbook"]').simulate('change', { wrapper.find('AnsibleSelect[name="playbook"]').simulate('change', {
target: { value: 'new baz type', name: 'playbook' }, target: { value: 'new baz type', name: 'playbook' },
}); });
@@ -221,6 +223,9 @@ describe('<JobTemplateForm />', () => {
expect(wrapper.find('input#template-scm-branch').prop('value')).toEqual( expect(wrapper.find('input#template-scm-branch').prop('value')).toEqual(
'devel' 'devel'
); );
expect(wrapper.find('input#template-limit').prop('value')).toEqual(
1234567890
);
expect( expect(
wrapper.find('AnsibleSelect[name="playbook"]').prop('value') wrapper.find('AnsibleSelect[name="playbook"]').prop('value')
).toEqual('new baz type'); ).toEqual('new baz type');

View File

@@ -31,8 +31,11 @@ options:
tower_oauthtoken: tower_oauthtoken:
description: description:
- The Tower OAuth token to use. - 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 - If value not set, will try environment variable C(TOWER_OAUTH_TOKEN) and then config files
type: str type: raw
version_added: "3.7" version_added: "3.7"
validate_certs: validate_certs:
description: description:

View File

@@ -12,7 +12,6 @@ DOCUMENTATION = '''
- Matthew Jones (@matburt) - Matthew Jones (@matburt)
- Yunfan Zhang (@YunfanZhang42) - Yunfan Zhang (@YunfanZhang42)
short_description: Ansible dynamic inventory plugin for Ansible Tower. short_description: Ansible dynamic inventory plugin for Ansible Tower.
version_added: "2.7"
description: description:
- Reads inventories from Ansible Tower. - Reads inventories from Ansible Tower.
- Supports reading configuration from both YAML config file and environment variables. - 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. 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. - If reading configurations from environment variables, the path in the command must be @tower_inventory.
options: 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: host:
description: The network address of your Ansible Tower host. description: The network address of your Ansible Tower host.
type: string
env: env:
- name: TOWER_HOST - name: TOWER_HOST
required: True
username: username:
description: The user that you plan to use to access inventories on Ansible Tower. description: The user that you plan to use to access inventories on Ansible Tower.
type: string
env: env:
- name: TOWER_USERNAME - name: TOWER_USERNAME
required: True
password: password:
description: The password for your Ansible Tower user. description: The password for your Ansible Tower user.
type: string
env: env:
- name: TOWER_PASSWORD - name: TOWER_PASSWORD
required: True oauth_token:
description:
- The Tower OAuth token to use.
env:
- name: TOWER_OAUTH_TOKEN
inventory_id: inventory_id:
description: description:
- The ID of the Ansible Tower inventory that you wish to import. - The ID of the Ansible Tower inventory that you wish to import.
@@ -56,19 +47,18 @@ DOCUMENTATION = '''
env: env:
- name: TOWER_INVENTORY - name: TOWER_INVENTORY
required: True required: True
validate_certs: verify_ssl:
description: Specify whether Ansible should verify the SSL certificate of Ansible Tower host. 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 type: bool
default: True
env: env:
- name: TOWER_VERIFY_SSL - name: TOWER_VERIFY_SSL
required: False aliases: [ validate_certs ]
aliases: [ verify_ssl ]
include_metadata: include_metadata:
description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host. description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host.
type: bool type: bool
default: False default: False
version_added: "2.8"
''' '''
EXAMPLES = ''' EXAMPLES = '''
@@ -99,7 +89,6 @@ inventory_id: the_ID_of_targeted_ansible_tower_inventory
''' '''
import os import os
import re
from ansible.module_utils import six from ansible.module_utils import six
from ansible.module_utils._text import to_text, to_native 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.plugins.inventory import BaseInventoryPlugin
from ansible.config.manager import ensure_type 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: def handle_error(**kwargs):
from urlparse import urljoin raise AnsibleParserError(to_native(kwargs.get('msg')))
except ImportError:
from urllib.parse import urljoin
class InventoryModule(BaseInventoryPlugin): class InventoryModule(BaseInventoryPlugin):
@@ -131,20 +118,25 @@ class InventoryModule(BaseInventoryPlugin):
else: else:
return False return False
def warn_callback(self, warning):
self.display.warning(warning)
def parse(self, inventory, loader, path, cache=True): def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path) super(InventoryModule, self).parse(inventory, loader, path)
if not self.no_config_file_supplied and os.path.isfile(path): if not self.no_config_file_supplied and os.path.isfile(path):
self._read_config_data(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'), # Defer processing of params to logic shared with the modules
url_password=self.get_option('password'), module_params = {}
force_basic_auth=True, for plugin_param, module_param in TowerModule.short_params.items():
validate_certs=self.get_option('validate_certs')) 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 # validate type of inventory_id because we allow two types as special case
inventory_id = self.get_option('inventory_id') 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)) 'not integer, and cannot convert to string: {err}'.format(err=to_native(e))
) )
inventory_id = inventory_id.replace('/', '') 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 = '/api/v2/inventories/{inv_id}/script/'.format(inv_id=inventory_id)
inventory_url = urljoin(tower_host, inventory_url)
try: inventory = module.get_endpoint(
inventory = make_request(request_handler, inventory_url) inventory_url, data={'hostvars': '1', 'towervars': '1', 'all': '1'}
except CollectionsParserError as e: )['json']
raise AnsibleParserError(to_native(e))
# To start with, create all the groups. # To start with, create all the groups.
for group_name in inventory: for group_name in inventory:
@@ -195,12 +185,8 @@ class InventoryModule(BaseInventoryPlugin):
# Fetch extra variables if told to do so # Fetch extra variables if told to do so
if self.get_option('include_metadata'): if self.get_option('include_metadata'):
config_url = urljoin(tower_host, '/api/v2/config/')
try: config_data = module.get_endpoint('/api/v2/config/')['json']
config_data = make_request(request_handler, config_url)
except CollectionsParserError as e:
raise AnsibleParserError(to_native(e))
server_data = {} server_data = {}
server_data['license_type'] = config_data.get('license_info', {}).get('license_type', 'unknown') server_data['license_type'] = config_data.get('license_info', {}).get('license_type', 'unknown')

View File

@@ -6,7 +6,6 @@ __metaclass__ = type
DOCUMENTATION = """ DOCUMENTATION = """
lookup: tower_schedule_rrule lookup: tower_schedule_rrule
author: John Westcott IV (@john-westcott-iv) author: John Westcott IV (@john-westcott-iv)
version_added: "3.7"
short_description: Generate an rrule string which can be used for Tower Schedules short_description: Generate an rrule string which can be used for Tower Schedules
requirements: requirements:
- pytz - pytz

View File

@@ -29,14 +29,9 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import json
import os import os
import traceback 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 TOWER_CLI_IMP_ERR = None
try: try:
import tower_cli.utils.exceptions as exc import tower_cli.utils.exceptions as exc
@@ -51,31 +46,6 @@ except ImportError:
from ansible.module_utils.basic import AnsibleModule, missing_required_lib 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): def tower_auth_config(module):
''' '''
`tower_auth_config` attempts to load the tower-cli.cfg file `tower_auth_config` attempts to load the tower-cli.cfg file

View File

@@ -3,7 +3,7 @@ __metaclass__ = type
from ansible.module_utils.basic import AnsibleModule, env_fallback from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError 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 import StringIO
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode
from ansible.module_utils.six.moves.urllib.error import HTTPError from ansible.module_utils.six.moves.urllib.error import HTTPError
@@ -42,7 +42,21 @@ class TowerModule(AnsibleModule):
'tower': 'Red Hat Ansible Tower', 'tower': 'Red Hat Ansible Tower',
} }
url = None 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' host = '127.0.0.1'
username = None username = None
password = None password = None
@@ -55,36 +69,46 @@ class TowerModule(AnsibleModule):
config_name = 'tower_cli.cfg' config_name = 'tower_cli.cfg'
ENCRYPTED_STRING = "$encrypted$" ENCRYPTED_STRING = "$encrypted$"
version_checked = False version_checked = False
error_callback = None
warn_callback = None
def __init__(self, argument_spec, **kwargs): def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs):
args = dict( full_argspec = {}
tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])), full_argspec.update(TowerModule.AUTH_ARGSPEC)
tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), full_argspec.update(argument_spec)
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)
kwargs['supports_check_mode'] = True kwargs['supports_check_mode'] = True
self.error_callback = error_callback
self.warn_callback = warn_callback
self.json_output = {'changed': False} 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() self.load_config_files()
# Parameters specified on command line will override settings in any config # Parameters specified on command line will override settings in any config
if self.params.get('tower_host'): for short_param, long_param in self.short_params.items():
self.host = self.params.get('tower_host') direct_value = self.params.get(long_param)
if self.params.get('tower_username'): if direct_value is not None:
self.username = self.params.get('tower_username') setattr(self, short_param, direct_value)
if self.params.get('tower_password'):
self.password = self.params.get('tower_password') # Perform magic depending on whether tower_oauthtoken is a string or a dict
if self.params.get('validate_certs') is not None:
self.verify_ssl = self.params.get('validate_certs')
if self.params.get('tower_oauthtoken'): 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 # Perform some basic validation
if not re.match('^https{0,1}://', self.host): 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 we have a specified tower config, load it
if self.params.get('tower_config_file'): if self.params.get('tower_config_file'):
duplicated_params = [] duplicated_params = [
for direct_field in ('tower_host', 'tower_username', 'tower_password', 'validate_certs', 'tower_oauthtoken'): fn for fn in self.AUTH_ARGSPEC
if self.params.get(direct_field): if fn != 'tower_config_file' and self.params.get(fn) is not None
duplicated_params.append(direct_field) ]
if duplicated_params: if duplicated_params:
self.warn(( self.warn((
'The parameter(s) {0} were provided at the same time as tower_config_file. ' '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 # 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 = {} config_data = {}
for honorred_setting in self.honorred_settings: for honorred_setting in self.short_params:
try: try:
config_data[honorred_setting] = config.get('general', honorred_setting) config_data[honorred_setting] = config.get('general', honorred_setting)
except NoOptionError: except NoOptionError:
@@ -197,7 +221,7 @@ class TowerModule(AnsibleModule):
raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e)) 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 # 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: if honorred_setting in config_data:
# Veriffy SSL must be a boolean # Veriffy SSL must be a boolean
if honorred_setting == 'verify_ssl': if honorred_setting == 'verify_ssl':
@@ -494,6 +518,9 @@ class TowerModule(AnsibleModule):
item_name = existing_item['username'] item_name = existing_item['username']
elif 'identifier' in existing_item: elif 'identifier' in existing_item:
item_name = existing_item['identifier'] 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: else:
self.fail_json(msg="Unable to process delete of {0} due to missing name".format(item_type)) 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): def fail_json(self, **kwargs):
# Try to log out if we are authenticated # Try to log out if we are authenticated
self.logout() 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): def exit_json(self, **kwargs):
# Try to log out if we are authenticated # Try to log out if we are authenticated
self.logout() self.logout()
super(TowerModule, self).exit_json(**kwargs) 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): def is_job_done(self, job_status):
if job_status in ['new', 'pending', 'waiting', 'running']: if job_status in ['new', 'pending', 'waiting', 'running']:
return False return False

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_credential module: tower_credential
author: "Wayne Witzel III (@wwitzel3)" author: "Wayne Witzel III (@wwitzel3)"
version_added: "2.3"
short_description: create, update, or destroy Ansible Tower credential. short_description: create, update, or destroy Ansible Tower credential.
description: description:
- Create, update, or destroy Ansible Tower credentials. See - Create, update, or destroy Ansible Tower credentials. See
@@ -45,7 +44,6 @@ options:
description: description:
- Name of credential type. - Name of credential type.
- Will be preferred over kind - Will be preferred over kind
version_added: "2.10"
type: str type: str
inputs: inputs:
description: description:
@@ -53,7 +51,6 @@ options:
Credential inputs where the keys are var names used in templating. Credential inputs where the keys are var names used in templating.
Refer to the Ansible Tower documentation for example syntax. 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) - Any fields in this dict will take prescedence over any fields mentioned below (i.e. host, username, etc)
version_added: "2.9"
type: dict type: dict
user: user:
description: description:
@@ -124,7 +121,6 @@ options:
description: description:
- STS token for aws type. - STS token for aws type.
- Deprecated, please use inputs - Deprecated, please use inputs
version_added: "2.6"
type: str type: str
secret: secret:
description: description:
@@ -177,7 +173,6 @@ options:
- This parameter is only valid if C(kind) is specified as C(vault). - This parameter is only valid if C(kind) is specified as C(vault).
- Deprecated, please use inputs - Deprecated, please use inputs
type: str type: str
version_added: "2.8"
state: state:
description: description:
- Desired state of the resource. - Desired state of the resource.
@@ -360,9 +355,9 @@ def main():
# Deprication warnings # Deprication warnings
for legacy_input in OLD_INPUT_NAMES: for legacy_input in OLD_INPUT_NAMES:
if module.params.get(legacy_input) is not None: 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: 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]) cred_type_id = module.resolve_name_to_id('credential_types', credential_type if credential_type else KIND_CHOICES[kind])
if organization: if organization:

View File

@@ -18,7 +18,6 @@ DOCUMENTATION = '''
--- ---
module: tower_credential_type module: tower_credential_type
author: "Adrien Fleury (@fleu42)" author: "Adrien Fleury (@fleu42)"
version_added: "2.7"
short_description: Create, update, or destroy custom Ansible Tower credential type. short_description: Create, update, or destroy custom Ansible Tower credential type.
description: description:
- Create, update, or destroy Ansible Tower credential type. See - Create, update, or destroy Ansible Tower credential type. See

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_group module: tower_group
author: "Wayne Witzel III (@wwitzel3)" author: "Wayne Witzel III (@wwitzel3)"
version_added: "2.3"
short_description: create, update, or destroy Ansible Tower group. short_description: create, update, or destroy Ansible Tower group.
description: description:
- Create, update, or destroy Ansible Tower groups. See - Create, update, or destroy Ansible Tower groups. See
@@ -63,7 +62,6 @@ options:
description: description:
- A new name for this group (for renaming) - A new name for this group (for renaming)
type: str type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''

View File

@@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: tower_host module: tower_host
version_added: "2.3"
author: "Wayne Witzel III (@wwitzel3)" author: "Wayne Witzel III (@wwitzel3)"
short_description: create, update, or destroy Ansible Tower host. short_description: create, update, or destroy Ansible Tower host.
description: description:
@@ -32,7 +31,6 @@ options:
description: description:
- To use when changing a hosts's name. - To use when changing a hosts's name.
type: str type: str
version_added: "3.7"
description: description:
description: description:
- The description to use for the host. - The description to use for the host.

View File

@@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: tower_inventory module: tower_inventory
version_added: "2.3"
author: "Wayne Witzel III (@wwitzel3)" author: "Wayne Witzel III (@wwitzel3)"
short_description: create, update, or destroy Ansible Tower inventory. short_description: create, update, or destroy Ansible Tower inventory.
description: description:
@@ -46,12 +45,10 @@ options:
- The kind field. Cannot be modified after created. - The kind field. Cannot be modified after created.
default: "" default: ""
choices: ["", "smart"] choices: ["", "smart"]
version_added: "2.7"
type: str type: str
host_filter: host_filter:
description: description:
- The host_filter field. Only useful when C(kind=smart). - The host_filter field. Only useful when C(kind=smart).
version_added: "2.7"
type: str type: str
state: state:
description: description:

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_inventory_source module: tower_inventory_source
author: "Adrien Fleury (@fleu42)" author: "Adrien Fleury (@fleu42)"
version_added: "2.7"
short_description: create, update, or destroy Ansible Tower inventory source. short_description: create, update, or destroy Ansible Tower inventory source.
description: description:
- Create, update, or destroy Ansible Tower inventory source. See - Create, update, or destroy Ansible Tower inventory source. See
@@ -32,7 +31,6 @@ options:
description: description:
- A new name for this assets (will rename the asset) - A new name for this assets (will rename the asset)
type: str type: str
version_added: "3.7"
description: description:
description: description:
- The description to use for the inventory source. - 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. - Override vars in child groups and hosts with those from external source.
type: bool type: bool
custom_virtualenv: custom_virtualenv:
version_added: "2.9"
description: description:
- Local absolute file path containing a custom Python virtualenv to use. - Local absolute file path containing a custom Python virtualenv to use.
type: str type: str

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_job_cancel module: tower_job_cancel
author: "Wayne Witzel III (@wwitzel3)" author: "Wayne Witzel III (@wwitzel3)"
version_added: "2.3"
short_description: Cancel an Ansible Tower Job. short_description: Cancel an Ansible Tower Job.
description: description:
- Cancel Ansible Tower jobs. See - Cancel Ansible Tower jobs. See

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_job_launch module: tower_job_launch
author: "Wayne Witzel III (@wwitzel3)" author: "Wayne Witzel III (@wwitzel3)"
version_added: "2.3"
short_description: Launch an Ansible Job. short_description: Launch an Ansible Job.
description: description:
- Launch an Ansible Tower jobs. See - Launch an Ansible Tower jobs. See
@@ -64,29 +63,24 @@ options:
- A specific of the SCM project to run the template on. - A specific of the SCM project to run the template on.
- This is only applicable if your project allows for branch override. - This is only applicable if your project allows for branch override.
type: str type: str
version_added: "3.7"
skip_tags: skip_tags:
description: description:
- Specific tags to skip from the playbook. - Specific tags to skip from the playbook.
type: list type: list
elements: str elements: str
version_added: "3.7"
verbosity: verbosity:
description: description:
- Verbosity level for this job run - Verbosity level for this job run
type: int type: int
choices: [ 0, 1, 2, 3, 4, 5 ] choices: [ 0, 1, 2, 3, 4, 5 ]
version_added: "3.7"
diff_mode: diff_mode:
description: description:
- Show the changes made by Ansible tasks where supported - Show the changes made by Ansible tasks where supported
type: bool type: bool
version_added: "3.7"
credential_passwords: credential_passwords:
description: description:
- Passwords for credentials which are set to prompt on launch - Passwords for credentials which are set to prompt on launch
type: dict type: dict
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_job_list module: tower_job_list
author: "Wayne Witzel III (@wwitzel3)" author: "Wayne Witzel III (@wwitzel3)"
version_added: "2.3"
short_description: List Ansible Tower jobs. short_description: List Ansible Tower jobs.
description: description:
- List Ansible Tower jobs. See - List Ansible Tower jobs. See

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_job_template module: tower_job_template
author: "Wayne Witzel III (@wwitzel3)" author: "Wayne Witzel III (@wwitzel3)"
version_added: "2.3"
short_description: create, update, or destroy Ansible Tower job templates. short_description: create, update, or destroy Ansible Tower job templates.
description: description:
- Create, update, or destroy Ansible Tower job templates. See - Create, update, or destroy Ansible Tower job templates. See
@@ -45,6 +44,14 @@ options:
description: description:
- Name of the inventory to use for the job template. - Name of the inventory to use for the job template.
type: str 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: project:
description: description:
- Name of the project to use for the job template. - Name of the project to use for the job template.
@@ -57,19 +64,16 @@ options:
description: description:
- Name of the credential to use for the job template. - Name of the credential to use for the job template.
- Deprecated, use 'credentials'. - Deprecated, use 'credentials'.
version_added: 2.7
type: str type: str
credentials: credentials:
description: description:
- List of credentials to use for the job template. - List of credentials to use for the job template.
type: list type: list
elements: str elements: str
version_added: 2.8
vault_credential: vault_credential:
description: description:
- Name of the vault credential to use for the job template. - Name of the vault credential to use for the job template.
- Deprecated, use 'credentials'. - Deprecated, use 'credentials'.
version_added: 2.7
type: str type: str
forks: forks:
description: description:
@@ -89,7 +93,6 @@ options:
description: description:
- Specify C(extra_vars) for the template. - Specify C(extra_vars) for the template.
type: dict type: dict
version_added: 3.7
job_tags: job_tags:
description: description:
- Comma separated list of the tags to use for the job template. - Comma separated list of the tags to use for the job template.
@@ -97,7 +100,6 @@ options:
force_handlers: force_handlers:
description: description:
- Enable forcing playbook handlers to run even if a task fails. - Enable forcing playbook handlers to run even if a task fails.
version_added: 2.7
type: bool type: bool
default: 'no' default: 'no'
aliases: aliases:
@@ -109,12 +111,10 @@ options:
start_at_task: start_at_task:
description: description:
- Start the playbook at the task matching this name. - Start the playbook at the task matching this name.
version_added: 2.7
type: str type: str
diff_mode: diff_mode:
description: description:
- Enable diff mode for the job template. - Enable diff mode for the job template.
version_added: 2.7
type: bool type: bool
aliases: aliases:
- diff_mode_enabled - diff_mode_enabled
@@ -122,7 +122,6 @@ options:
use_fact_cache: use_fact_cache:
description: description:
- Enable use of fact caching for the job template. - Enable use of fact caching for the job template.
version_added: 2.7
type: bool type: bool
default: 'no' default: 'no'
aliases: aliases:
@@ -139,7 +138,6 @@ options:
ask_diff_mode_on_launch: ask_diff_mode_on_launch:
description: description:
- Prompt user to enable diff mode (show changes) to files when supported by modules. - Prompt user to enable diff mode (show changes) to files when supported by modules.
version_added: 2.7
type: bool type: bool
default: 'False' default: 'False'
aliases: aliases:
@@ -154,7 +152,6 @@ options:
ask_limit_on_launch: ask_limit_on_launch:
description: description:
- Prompt user for a limit on launch. - Prompt user for a limit on launch.
version_added: 2.7
type: bool type: bool
default: 'False' default: 'False'
aliases: aliases:
@@ -169,7 +166,6 @@ options:
ask_skip_tags_on_launch: ask_skip_tags_on_launch:
description: description:
- Prompt user for job tags to skip on launch. - Prompt user for job tags to skip on launch.
version_added: 2.7
type: bool type: bool
default: 'False' default: 'False'
aliases: aliases:
@@ -184,7 +180,6 @@ options:
ask_verbosity_on_launch: ask_verbosity_on_launch:
description: description:
- Prompt user to choose a verbosity level on launch. - Prompt user to choose a verbosity level on launch.
version_added: 2.7
type: bool type: bool
default: 'False' default: 'False'
aliases: aliases:
@@ -206,13 +201,11 @@ options:
survey_enabled: survey_enabled:
description: description:
- Enable a survey on the job template. - Enable a survey on the job template.
version_added: 2.7
type: bool type: bool
default: 'no' default: 'no'
survey_spec: survey_spec:
description: description:
- JSON/YAML dict formatted survey definition. - JSON/YAML dict formatted survey definition.
version_added: 2.8
type: dict type: dict
become_enabled: become_enabled:
description: description:
@@ -222,7 +215,6 @@ options:
allow_simultaneous: allow_simultaneous:
description: description:
- Allow simultaneous runs of the job template. - Allow simultaneous runs of the job template.
version_added: 2.7
type: bool type: bool
default: 'no' default: 'no'
aliases: aliases:
@@ -232,7 +224,6 @@ options:
- Maximum time in seconds to wait for a job to finish (server-side). - Maximum time in seconds to wait for a job to finish (server-side).
type: int type: int
custom_virtualenv: custom_virtualenv:
version_added: "2.9"
description: description:
- Local absolute file path containing a custom Python virtualenv to use. - Local absolute file path containing a custom Python virtualenv to use.
type: str type: str
@@ -299,6 +290,7 @@ EXAMPLES = '''
tower_job_template: tower_job_template:
name: "Ping" name: "Ping"
job_type: "run" job_type: "run"
organization: "Default"
inventory: "Local" inventory: "Local"
project: "Demo" project: "Demo"
playbook: "ping.yml" playbook: "ping.yml"
@@ -349,6 +341,7 @@ def main():
name=dict(required=True), name=dict(required=True),
new_name=dict(), new_name=dict(),
description=dict(default=''), description=dict(default=''),
organization=dict(),
job_type=dict(choices=['run', 'check']), job_type=dict(choices=['run', 'check']),
inventory=dict(), inventory=dict(),
project=dict(), project=dict(),
@@ -415,19 +408,24 @@ def main():
credentials = [] credentials = []
credentials.append(credential) 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 # Attempt to look up an existing item based on the provided data
existing_item = module.get_one('job_templates', **{ existing_item = module.get_one('job_templates', **{'data': search_fields})
'data': {
'name': name,
}
})
if state == 'absent': if state == 'absent':
# If the state was absent we can let the module delete it if needed, the module will handle exiting from this # 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) module.delete_if_needed(existing_item)
# Create the data that gets sent for create and update # Create the data that gets sent for create and update
new_fields = {}
new_fields['name'] = new_name if new_name else name new_fields['name'] = new_name if new_name else name
for field_name in ( for field_name in (
'description', 'job_type', 'playbook', 'scm_branch', 'forks', 'limit', 'verbosity', 'description', 'job_type', 'playbook', 'scm_branch', 'forks', 'limit', 'verbosity',
@@ -454,7 +452,20 @@ def main():
if inventory is not None: if inventory is not None:
new_fields['inventory'] = module.resolve_name_to_id('inventories', inventory) new_fields['inventory'] = module.resolve_name_to_id('inventories', inventory)
if project is not None: 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: if webhook_credential is not None:
new_fields['webhook_credential'] = module.resolve_name_to_id('credentials', webhook_credential) new_fields['webhook_credential'] = module.resolve_name_to_id('credentials', webhook_credential)

View File

@@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: tower_job_wait module: tower_job_wait
version_added: "2.3"
author: "Wayne Witzel III (@wwitzel3)" author: "Wayne Witzel III (@wwitzel3)"
short_description: Wait for Ansible Tower job to finish. short_description: Wait for Ansible Tower job to finish.
description: description:
@@ -141,7 +140,7 @@ def main():
interval = abs((min_interval + max_interval) / 2) interval = abs((min_interval + max_interval) / 2)
module.deprecate( module.deprecate(
msg="Min and max interval have been deprecated, please use interval instead; interval will be set to {0}".format(interval), 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 # Attempt to look up job based on the provided id

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_label module: tower_label
author: "Wayne Witzel III (@wwitzel3)" author: "Wayne Witzel III (@wwitzel3)"
version_added: "2.3"
short_description: create, update, or destroy Ansible Tower labels. short_description: create, update, or destroy Ansible Tower labels.
description: description:
- Create, update, or destroy Ansible Tower labels. See - Create, update, or destroy Ansible Tower labels. See

View File

@@ -16,7 +16,6 @@ DOCUMENTATION = '''
--- ---
module: tower_license module: tower_license
author: "John Westcott IV (@john-westcott-iv)" author: "John Westcott IV (@john-westcott-iv)"
version_added: "2.9"
short_description: Set the license for Ansible Tower short_description: Set the license for Ansible Tower
description: description:
- Get or Set Ansible Tower license. See - Get or Set Ansible Tower license. See
@@ -27,13 +26,11 @@ options:
- The contents of the license file - The contents of the license file
required: True required: True
type: dict type: dict
version_added: "3.7"
eula_accepted: eula_accepted:
description: description:
- Whether or not the EULA is accepted. - Whether or not the EULA is accepted.
required: True required: True
type: bool type: bool
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_notification module: tower_notification
author: "Samuel Carpentier (@samcarpentier)" author: "Samuel Carpentier (@samcarpentier)"
version_added: "2.8"
short_description: create, update, or destroy Ansible Tower notification. short_description: create, update, or destroy Ansible Tower notification.
description: description:
- Create, update, or destroy Ansible Tower notifications. See - Create, update, or destroy Ansible Tower notifications. See
@@ -371,7 +370,9 @@ def main():
# Deprecation warnings for all other params # Deprecation warnings for all other params
for legacy_input in OLD_INPUT_NAMES: for legacy_input in OLD_INPUT_NAMES:
if module.params.get(legacy_input) is not None: 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) # Attempt to look up the related items the user specified (these will fail the module if not found)
organization_id = None organization_id = None

View File

@@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: tower_organization module: tower_organization
version_added: "2.3"
author: "Wayne Witzel III (@wwitzel3)" author: "Wayne Witzel III (@wwitzel3)"
short_description: create, update, or destroy Ansible Tower organizations short_description: create, update, or destroy Ansible Tower organizations
description: description:
@@ -33,7 +32,6 @@ options:
- The description to use for the organization. - The description to use for the organization.
type: str type: str
custom_virtualenv: custom_virtualenv:
version_added: "2.9"
description: description:
- Local absolute file path containing a custom Python virtualenv to use. - Local absolute file path containing a custom Python virtualenv to use.
type: str type: str
@@ -43,7 +41,6 @@ options:
- The max hosts allowed in this organizations - The max hosts allowed in this organizations
default: "0" default: "0"
type: int type: int
version_added: "3.7"
state: state:
description: description:
- Desired state of the resource. - Desired state of the resource.

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_project module: tower_project
author: "Wayne Witzel III (@wwitzel3)" author: "Wayne Witzel III (@wwitzel3)"
version_added: "2.3"
short_description: create, update, or destroy Ansible Tower projects short_description: create, update, or destroy Ansible Tower projects
description: description:
- Create, update, or destroy Ansible Tower projects. See - Create, update, or destroy Ansible Tower projects. See
@@ -56,7 +55,6 @@ options:
- The refspec to use for the SCM resource. - The refspec to use for the SCM resource.
type: str type: str
default: '' default: ''
version_added: "3.7"
scm_credential: scm_credential:
description: description:
- Name of the credential to use with this SCM resource. - Name of the credential to use with this SCM resource.
@@ -77,7 +75,6 @@ options:
type: bool type: bool
default: 'no' default: 'no'
scm_update_cache_timeout: scm_update_cache_timeout:
version_added: "2.8"
description: description:
- Cache Timeout to cache prior project syncs for a certain number of seconds. - 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. Only valid if scm_update_on_launch is to True, otherwise ignored.
@@ -87,17 +84,14 @@ options:
description: description:
- Allow changing the SCM branch or revision in a job template that uses this project. - Allow changing the SCM branch or revision in a job template that uses this project.
type: bool type: bool
version_added: "3.7"
aliases: aliases:
- scm_allow_override - scm_allow_override
job_timeout: job_timeout:
version_added: "2.8"
description: description:
- The amount of time (in seconds) to run before the SCM Update is canceled. A value of 0 means no timeout. - The amount of time (in seconds) to run before the SCM Update is canceled. A value of 0 means no timeout.
default: 0 default: 0
type: int type: int
custom_virtualenv: custom_virtualenv:
version_added: "2.8"
description: description:
- Local absolute file path containing a custom Python virtualenv to use - Local absolute file path containing a custom Python virtualenv to use
type: str type: str

View File

@@ -17,11 +17,10 @@ DOCUMENTATION = '''
--- ---
module: tower_receive module: tower_receive
deprecated: deprecated:
removed_in: "3.7" removed_in: "14.0.0"
why: Deprecated in favor of upcoming C(_export) module. why: Deprecated in favor of upcoming C(_export) module.
alternative: Once published, use M(tower_export) instead. alternative: Once published, use M(tower_export) instead.
author: "John Westcott IV (@john-westcott-iv)" author: "John Westcott IV (@john-westcott-iv)"
version_added: "2.8"
short_description: Receive assets from Ansible Tower. short_description: Receive assets from Ansible Tower.
description: description:
- Receive assets from Ansible Tower. See - Receive assets from Ansible Tower. See
@@ -166,7 +165,7 @@ def main():
module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) 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: if not HAS_TOWER_CLI:
module.fail_json(msg='ansible-tower-cli required for this module') module.fail_json(msg='ansible-tower-cli required for this module')

View File

@@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: tower_role module: tower_role
version_added: "2.3"
author: "Wayne Witzel III (@wwitzel3)" author: "Wayne Witzel III (@wwitzel3)"
short_description: grant or revoke an Ansible Tower role. short_description: grant or revoke an Ansible Tower role.
description: description:

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_schedule module: tower_schedule
author: "John Westcott IV (@john-westcott-iv)" author: "John Westcott IV (@john-westcott-iv)"
version_added: "2.3"
short_description: create, update, or destroy Ansible Tower schedules. short_description: create, update, or destroy Ansible Tower schedules.
description: description:
- Create, update, or destroy Ansible Tower schedules. See - Create, update, or destroy Ansible Tower schedules. See

View File

@@ -17,11 +17,10 @@ DOCUMENTATION = '''
--- ---
module: tower_send module: tower_send
deprecated: deprecated:
removed_in: "3.7" removed_in: "14.0.0"
why: Deprecated in favor of upcoming C(_import) module. why: Deprecated in favor of upcoming C(_import) module.
alternative: Once published, use M(tower_import) instead. alternative: Once published, use M(tower_import) instead.
author: "John Westcott IV (@john-westcott-iv)" author: "John Westcott IV (@john-westcott-iv)"
version_added: "2.8"
short_description: Send assets to Ansible Tower. short_description: Send assets to Ansible Tower.
description: description:
- Send assets to Ansible Tower. See - Send assets to Ansible Tower. See
@@ -106,7 +105,7 @@ def main():
module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) 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: if not HAS_TOWER_CLI:
module.fail_json(msg='ansible-tower-cli required for this module') module.fail_json(msg='ansible-tower-cli required for this module')

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_settings module: tower_settings
author: "Nikhil Jain (@jainnikhil30)" author: "Nikhil Jain (@jainnikhil30)"
version_added: "2.7"
short_description: Modify Ansible Tower settings. short_description: Modify Ansible Tower settings.
description: description:
- Modify Ansible Tower settings. See - Modify Ansible Tower settings. See
@@ -37,7 +36,6 @@ options:
description: description:
- A data structure to be sent into the settings endpoint - A data structure to be sent into the settings endpoint
type: dict type: dict
version_added: "3.7"
requirements: requirements:
- pyyaml - pyyaml
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
@@ -82,6 +80,10 @@ except ImportError:
def coerce_type(module, value): 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(( yaml_ish = bool((
value.startswith('{') and value.endswith('}') value.startswith('{') and value.endswith('}')
) or ( ) or (

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_team module: tower_team
author: "Wayne Witzel III (@wwitzel3)" author: "Wayne Witzel III (@wwitzel3)"
version_added: "2.3"
short_description: create, update, or destroy Ansible Tower team. short_description: create, update, or destroy Ansible Tower team.
description: description:
- Create, update, or destroy Ansible Tower teams. See - Create, update, or destroy Ansible Tower teams. See
@@ -32,7 +31,6 @@ options:
description: description:
- To use when changing a team's name. - To use when changing a team's name.
type: str type: str
version_added: "3.7"
description: description:
description: description:
- The description to use for the team. - The description to use for the team.

View File

@@ -0,0 +1,201 @@
#!/usr/bin/python
# coding: utf-8 -*-
# (c) 2020, John Westcott IV <john.westcott.iv@redhat.com>
# 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()

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_user module: tower_user
author: "John Westcott IV (@john-westcott-iv)" author: "John Westcott IV (@john-westcott-iv)"
version_added: "2.3"
short_description: create, update, or destroy Ansible Tower users. short_description: create, update, or destroy Ansible Tower users.
description: description:
- Create, update, or destroy Ansible Tower users. See - Create, update, or destroy Ansible Tower users. See

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_workflow_job_template module: tower_workflow_job_template
author: "John Westcott IV (@john-westcott-iv)" author: "John Westcott IV (@john-westcott-iv)"
version_added: "2.3"
short_description: create, update, or destroy Ansible Tower workflow job templates. short_description: create, update, or destroy Ansible Tower workflow job templates.
description: description:
- Create, update, or destroy Ansible Tower workflow job templates. - Create, update, or destroy Ansible Tower workflow job templates.

View File

@@ -17,7 +17,6 @@ DOCUMENTATION = '''
--- ---
module: tower_workflow_job_template_node module: tower_workflow_job_template_node
author: "John Westcott IV (@john-westcott-iv)" author: "John Westcott IV (@john-westcott-iv)"
version_added: "2.3"
short_description: create, update, or destroy Ansible Tower workflow job template nodes. short_description: create, update, or destroy Ansible Tower workflow job template nodes.
description: description:
- Create, update, or destroy Ansible Tower workflow job template nodes. - Create, update, or destroy Ansible Tower workflow job template nodes.

View File

@@ -14,7 +14,6 @@ DOCUMENTATION = '''
--- ---
module: tower_workflow_launch module: tower_workflow_launch
author: "John Westcott IV (@john-westcott-iv)" author: "John Westcott IV (@john-westcott-iv)"
version_added: "2.8"
short_description: Run a workflow in Ansible Tower short_description: Run a workflow in Ansible Tower
description: description:
- Launch an Ansible Tower workflows. See - Launch an Ansible Tower workflows. See
@@ -32,7 +31,6 @@ options:
- Organization the workflow job template exists in. - Organization the workflow job template exists in.
- Used to help lookup the object, cannot be modified using this module. - 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. - If not provided, will lookup by name only, which does not work with duplicates.
required: False
type: str type: str
inventory: inventory:
description: description:
@@ -47,7 +45,6 @@ options:
- A specific branch of the SCM project to run the template on. - A specific branch of the SCM project to run the template on.
- This is only applicable if your project allows for branch override. - This is only applicable if your project allows for branch override.
type: str type: str
version_added: "3.7"
extra_vars: extra_vars:
description: description:
- Any extra vars required to launch the job. - Any extra vars required to launch the job.

View File

@@ -17,11 +17,10 @@ DOCUMENTATION = '''
--- ---
module: tower_workflow_template module: tower_workflow_template
deprecated: 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. 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. alternative: Use M(tower_workflow_job_template) and M(_workflow_job_template_node) instead.
author: "Adrien Fleury (@fleu42)" author: "Adrien Fleury (@fleu42)"
version_added: "2.7"
short_description: create, update, or destroy Ansible Tower workflow template. short_description: create, update, or destroy Ansible Tower workflow template.
description: description:
- A tower-cli based module for CRUD actions on workflow job templates. - A tower-cli based module for CRUD actions on workflow job templates.
@@ -37,12 +36,10 @@ options:
description: description:
- Prompt user for (extra_vars) on launch. - Prompt user for (extra_vars) on launch.
type: bool type: bool
version_added: "2.9"
ask_inventory: ask_inventory:
description: description:
- Prompt user for inventory on launch. - Prompt user for inventory on launch.
type: bool type: bool
version_added: "2.9"
description: description:
description: description:
- The description to use for the workflow. - The description to use for the workflow.
@@ -54,7 +51,6 @@ options:
inventory: inventory:
description: description:
- Name of the inventory to use for the job template. - Name of the inventory to use for the job template.
version_added: "2.9"
type: str type: str
name: name:
description: description:
@@ -153,7 +149,7 @@ def main():
"This module is replaced by the combination of tower_workflow_job_template and " "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 " "tower_workflow_job_template_node. This uses the old tower-cli and wll be "
"removed in 2022." "removed in 2022."
), version='4.2.0') ), version='awx.awx:14.0.0')
name = module.params.get('name') name = module.params.get('name')
state = module.params.get('state') state = module.params.get('state')

View File

@@ -71,21 +71,14 @@ def test_duplicate_config(collection_import, silence_warning):
'tower_config_file': 'my_config' 'tower_config_file': 'my_config'
} }
class DuplicateTestTowerModule(TowerModule): with mock.patch.object(TowerModule, 'load_config') as mock_load:
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):
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(required=True),
zig=dict(type='str'), 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( silence_warning.assert_called_once_with(
'The parameter(s) tower_username were provided at the same time as ' 'The parameter(s) tower_username were provided at the same time as '
'tower_config_file. Precedence may be unstable, ' 'tower_config_file. Precedence may be unstable, '

View File

@@ -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'

View File

@@ -74,3 +74,14 @@
- assert: - assert:
that: that:
- "result is changed" - "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"

View File

@@ -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

View File

@@ -1,6 +1,6 @@
plugins/modules/tower_receive.py validate-modules:deprecation-mismatch 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: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: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

View File

@@ -24,7 +24,7 @@ DOCUMENTATION = '''
--- ---
module: tower_{{ singular_item_type }} module: tower_{{ singular_item_type }}
author: "John Westcott IV (@john-westcott-iv)" 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 }}. short_description: create, update, or destroy Ansible Tower {{ human_readable }}.
description: description:
- Create, update, or destroy Ansible Tower {{ human_readable }}. See - Create, update, or destroy Ansible Tower {{ human_readable }}. See
@@ -87,7 +87,6 @@ options:
- The Tower OAuth token to use. - The Tower OAuth token to use.
required: False required: False
type: str type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''

View File

@@ -30,6 +30,13 @@
path: "{{ collection_path }}/plugins/inventory/tower.py" path: "{{ collection_path }}/plugins/inventory/tower.py"
regexp: "^ NAME = 'awx.awx.tower' # REPLACE$" regexp: "^ NAME = 'awx.awx.tower' # REPLACE$"
replace: " NAME = '{{ collection_namespace }}.{{ collection_package }}.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: when:
- (collection_package != 'awx') or (collection_namespace != 'awx') - (collection_package != 'awx') or (collection_namespace != 'awx')

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -220,7 +220,6 @@ RUN for dir in \
/vendor ; \ /vendor ; \
do mkdir -m 0775 -p $dir ; chmod g+rw $dir ; chgrp root $dir ; done && \ do mkdir -m 0775 -p $dir ; chmod g+rw $dir ; chgrp root $dir ; done && \
for file in \ for file in \
/etc/supervisord.conf \
/var/run/nginx.pid \ /var/run/nginx.pid \
/venv/awx/lib/python3.6/site-packages/awx.egg-link ; \ /venv/awx/lib/python3.6/site-packages/awx.egg-link ; \
do touch $file ; chmod g+rw $file ; done do touch $file ; chmod g+rw $file ; done

View File

@@ -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 if [ -z "$AWX_SKIP_MIGRATIONS" ]; then
awx-manage migrate --noinput awx-manage migrate --noinput
awx-manage provision_instance --hostname=$(hostname)
awx-manage register_queue --queuename=tower --instance_percent=100
fi fi
if [ ! -z "$AWX_ADMIN_USER" ]&&[ ! -z "$AWX_ADMIN_PASSWORD" ]; then 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 %} {% endif %}
fi 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 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) unset $(cut -d = -f -1 /etc/tower/conf.d/environment.sh)

View File

@@ -7,7 +7,7 @@ collections:
- name: amazon.aws - name: amazon.aws
version: 0.1.1 # version 0.1.0 seems to have gone missing version: 0.1.1 # version 0.1.0 seems to have gone missing
- name: theforeman.foreman - name: theforeman.foreman
version: 0.8.0 version: 0.8.1
- name: google.cloud - name: google.cloud
version: 0.0.9 # contains PR 167, should be good to go version: 0.0.9 # contains PR 167, should be good to go
- name: openstack.cloud - name: openstack.cloud

View File

@@ -31,6 +31,7 @@ services:
- "../:/awx_devel" - "../:/awx_devel"
- "./redis/redis_socket_ha_1:/var/run/redis/" - "./redis/redis_socket_ha_1:/var/run/redis/"
- "./memcached/:/var/run/memcached" - "./memcached/:/var/run/memcached"
- "./docker-compose/supervisor.conf:/etc/supervisord.conf"
ports: ports:
- "5899-5999:5899-5999" - "5899-5999:5899-5999"
awx-2: awx-2:
@@ -50,6 +51,7 @@ services:
- "../:/awx_devel" - "../:/awx_devel"
- "./redis/redis_socket_ha_2:/var/run/redis/" - "./redis/redis_socket_ha_2:/var/run/redis/"
- "./memcached/:/var/run/memcached" - "./memcached/:/var/run/memcached"
- "./docker-compose/supervisor.conf:/etc/supervisord.conf"
ports: ports:
- "7899-7999:7899-7999" - "7899-7999:7899-7999"
awx-3: awx-3:
@@ -69,6 +71,7 @@ services:
- "../:/awx_devel" - "../:/awx_devel"
- "./redis/redis_socket_ha_3:/var/run/redis/" - "./redis/redis_socket_ha_3:/var/run/redis/"
- "./memcached/:/var/run/memcached" - "./memcached/:/var/run/memcached"
- "./docker-compose/supervisor.conf:/etc/supervisord.conf"
ports: ports:
- "8899-8999:8899-8999" - "8899-8999:8899-8999"
redis_1: redis_1:

View File

@@ -34,7 +34,7 @@ services:
- "../awx/projects/:/var/lib/awx/projects/" - "../awx/projects/:/var/lib/awx/projects/"
- "./redis/redis_socket_standalone:/var/run/redis/" - "./redis/redis_socket_standalone:/var/run/redis/"
- "./memcached/:/var/run/memcached" - "./memcached/:/var/run/memcached"
- "./rsyslog/:/var/lib/awx/rsyslog" - "./docker-compose/supervisor.conf:/etc/supervisord.conf"
privileged: true privileged: true
tty: true tty: true
# A useful container that simply passes through log messages to the console # A useful container that simply passes through log messages to the console

View File

@@ -20,7 +20,6 @@ else
fi fi
make awx-link make awx-link
yes | cp -rf /awx_devel/tools/docker-compose/supervisor.conf /etc/supervisord.conf
# AWX bootstrapping # AWX bootstrapping
make version_file make version_file