mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 03:10:42 -03:30
commit
104073af45
@ -50,9 +50,9 @@ conjur_inputs = {
|
||||
def conjur_backend(**kwargs):
|
||||
url = kwargs['url']
|
||||
api_key = kwargs['api_key']
|
||||
account = quote(kwargs['account'])
|
||||
username = quote(kwargs['username'])
|
||||
secret_path = quote(kwargs['secret_path'])
|
||||
account = quote(kwargs['account'], safe='')
|
||||
username = quote(kwargs['username'], safe='')
|
||||
secret_path = quote(kwargs['secret_path'], safe='')
|
||||
version = kwargs.get('secret_version')
|
||||
cacert = kwargs.get('cacert', None)
|
||||
|
||||
|
||||
@ -35,6 +35,7 @@ class WorkerSignalHandler:
|
||||
|
||||
def __init__(self):
|
||||
self.kill_now = False
|
||||
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
||||
signal.signal(signal.SIGINT, self.exit_gracefully)
|
||||
|
||||
def exit_gracefully(self, *args, **kwargs):
|
||||
|
||||
@ -16,31 +16,24 @@ class InstanceNotFound(Exception):
|
||||
super(InstanceNotFound, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
class RegisterQueue:
|
||||
def __init__(self, queuename, controller, instance_percent, inst_min, hostname_list):
|
||||
self.instance_not_found_err = None
|
||||
self.queuename = queuename
|
||||
self.controller = controller
|
||||
self.instance_percent = instance_percent
|
||||
self.instance_min = inst_min
|
||||
self.hostname_list = hostname_list
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--queuename', dest='queuename', type=str,
|
||||
help='Queue to create/update')
|
||||
parser.add_argument('--hostnames', dest='hostnames', type=str,
|
||||
help='Comma-Delimited Hosts to add to the Queue (will not remove already assigned instances)')
|
||||
parser.add_argument('--controller', dest='controller', type=str,
|
||||
default='', help='The controlling group (makes this an isolated group)')
|
||||
parser.add_argument('--instance_percent', dest='instance_percent', type=int, default=0,
|
||||
help='The percentage of active instances that will be assigned to this group'),
|
||||
parser.add_argument('--instance_minimum', dest='instance_minimum', type=int, default=0,
|
||||
help='The minimum number of instance that will be retained for this group from available instances')
|
||||
|
||||
|
||||
def get_create_update_instance_group(self, queuename, instance_percent, instance_min):
|
||||
def get_create_update_instance_group(self):
|
||||
created = False
|
||||
changed = False
|
||||
|
||||
(ig, created) = InstanceGroup.objects.get_or_create(name=queuename)
|
||||
if ig.policy_instance_percentage != instance_percent:
|
||||
ig.policy_instance_percentage = instance_percent
|
||||
(ig, created) = InstanceGroup.objects.get_or_create(name=self.queuename)
|
||||
if ig.policy_instance_percentage != self.instance_percent:
|
||||
ig.policy_instance_percentage = self.instance_percent
|
||||
changed = True
|
||||
if ig.policy_instance_minimum != instance_min:
|
||||
ig.policy_instance_minimum = instance_min
|
||||
if ig.policy_instance_minimum != self.instance_min:
|
||||
ig.policy_instance_minimum = self.instance_min
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
@ -48,12 +41,12 @@ class Command(BaseCommand):
|
||||
|
||||
return (ig, created, changed)
|
||||
|
||||
def update_instance_group_controller(self, ig, controller):
|
||||
def update_instance_group_controller(self, ig):
|
||||
changed = False
|
||||
control_ig = None
|
||||
|
||||
if controller:
|
||||
control_ig = InstanceGroup.objects.filter(name=controller).first()
|
||||
if self.controller:
|
||||
control_ig = InstanceGroup.objects.filter(name=self.controller).first()
|
||||
|
||||
if control_ig and ig.controller_id != control_ig.pk:
|
||||
ig.controller = control_ig
|
||||
@ -62,10 +55,10 @@ class Command(BaseCommand):
|
||||
|
||||
return (control_ig, changed)
|
||||
|
||||
def add_instances_to_group(self, ig, hostname_list):
|
||||
def add_instances_to_group(self, ig):
|
||||
changed = False
|
||||
|
||||
instance_list_unique = set([x.strip() for x in hostname_list if x])
|
||||
instance_list_unique = set([x.strip() for x in self.hostname_list if x])
|
||||
instances = []
|
||||
for inst_name in instance_list_unique:
|
||||
instance = Instance.objects.filter(hostname=inst_name)
|
||||
@ -86,43 +79,61 @@ class Command(BaseCommand):
|
||||
|
||||
return (instances, changed)
|
||||
|
||||
def handle(self, **options):
|
||||
instance_not_found_err = None
|
||||
queuename = options.get('queuename')
|
||||
if not queuename:
|
||||
raise CommandError("Specify `--queuename` to use this command.")
|
||||
ctrl = options.get('controller')
|
||||
inst_per = options.get('instance_percent')
|
||||
inst_min = options.get('instance_minimum')
|
||||
hostname_list = []
|
||||
if options.get('hostnames'):
|
||||
hostname_list = options.get('hostnames').split(",")
|
||||
|
||||
def register(self):
|
||||
with advisory_lock('cluster_policy_lock'):
|
||||
with transaction.atomic():
|
||||
changed2 = False
|
||||
changed3 = False
|
||||
(ig, created, changed1) = self.get_create_update_instance_group(queuename, inst_per, inst_min)
|
||||
(ig, created, changed1) = self.get_create_update_instance_group()
|
||||
if created:
|
||||
print("Creating instance group {}".format(ig.name))
|
||||
elif not created:
|
||||
print("Instance Group already registered {}".format(ig.name))
|
||||
|
||||
if ctrl:
|
||||
(ig_ctrl, changed2) = self.update_instance_group_controller(ig, ctrl)
|
||||
if self.controller:
|
||||
(ig_ctrl, changed2) = self.update_instance_group_controller(ig)
|
||||
if changed2:
|
||||
print("Set controller group {} on {}.".format(ctrl, queuename))
|
||||
print("Set controller group {} on {}.".format(self.controller, self.queuename))
|
||||
|
||||
try:
|
||||
(instances, changed3) = self.add_instances_to_group(ig, hostname_list)
|
||||
(instances, changed3) = self.add_instances_to_group(ig)
|
||||
for i in instances:
|
||||
print("Added instance {} to {}".format(i.hostname, ig.name))
|
||||
except InstanceNotFound as e:
|
||||
instance_not_found_err = e
|
||||
self.instance_not_found_err = e
|
||||
|
||||
if any([changed1, changed2, changed3]):
|
||||
print('(changed: True)')
|
||||
|
||||
if instance_not_found_err:
|
||||
print(instance_not_found_err.message)
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--queuename', dest='queuename', type=str,
|
||||
help='Queue to create/update')
|
||||
parser.add_argument('--hostnames', dest='hostnames', type=str,
|
||||
help='Comma-Delimited Hosts to add to the Queue (will not remove already assigned instances)')
|
||||
parser.add_argument('--controller', dest='controller', type=str,
|
||||
default='', help='The controlling group (makes this an isolated group)')
|
||||
parser.add_argument('--instance_percent', dest='instance_percent', type=int, default=0,
|
||||
help='The percentage of active instances that will be assigned to this group'),
|
||||
parser.add_argument('--instance_minimum', dest='instance_minimum', type=int, default=0,
|
||||
help='The minimum number of instance that will be retained for this group from available instances')
|
||||
|
||||
|
||||
def handle(self, **options):
|
||||
queuename = options.get('queuename')
|
||||
if not queuename:
|
||||
raise CommandError("Specify `--queuename` to use this command.")
|
||||
ctrl = options.get('controller')
|
||||
inst_per = options.get('instance_percent')
|
||||
instance_min = options.get('instance_minimum')
|
||||
hostname_list = []
|
||||
if options.get('hostnames'):
|
||||
hostname_list = options.get('hostnames').split(",")
|
||||
|
||||
rq = RegisterQueue(queuename, ctrl, inst_per, instance_min, hostname_list)
|
||||
rq.register()
|
||||
if rq.instance_not_found_err:
|
||||
print(rq.instance_not_found_err.message)
|
||||
sys.exit(1)
|
||||
|
||||
@ -149,8 +149,11 @@ class InstanceManager(models.Manager):
|
||||
|
||||
def get_or_register(self):
|
||||
if settings.AWX_AUTO_DEPROVISION_INSTANCES:
|
||||
from awx.main.management.commands.register_queue import RegisterQueue
|
||||
pod_ip = os.environ.get('MY_POD_IP')
|
||||
return self.register(ip_address=pod_ip)
|
||||
registered = self.register(ip_address=pod_ip)
|
||||
RegisterQueue('tower', None, 100, 0, []).register()
|
||||
return registered
|
||||
else:
|
||||
return (False, self.me())
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
|
||||
import logging
|
||||
import requests
|
||||
import json
|
||||
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@ -45,7 +44,7 @@ class MattermostBackend(AWXBaseEmailBackend, CustomNotificationBase):
|
||||
payload['text'] = m.subject
|
||||
|
||||
r = requests.post("{}".format(m.recipients()[0]),
|
||||
data=json.dumps(payload), verify=(not self.mattermost_no_verify_ssl))
|
||||
json=payload, verify=(not self.mattermost_no_verify_ssl))
|
||||
if r.status_code >= 400:
|
||||
logger.error(smart_text(_("Error sending notification mattermost: {}").format(r.text)))
|
||||
if not self.fail_silently:
|
||||
|
||||
@ -581,3 +581,4 @@ class TaskManager():
|
||||
logger.debug("Starting Scheduler")
|
||||
with task_manager_bulk_reschedule():
|
||||
self._schedule()
|
||||
logger.debug("Finishing Scheduler")
|
||||
|
||||
@ -13,6 +13,10 @@ class RSysLogHandler(logging.handlers.SysLogHandler):
|
||||
|
||||
append_nul = False
|
||||
|
||||
def _connect_unixsocket(self, address):
|
||||
super(RSysLogHandler, self)._connect_unixsocket(address)
|
||||
self.socket.setblocking(False)
|
||||
|
||||
def emit(self, msg):
|
||||
if not settings.LOG_AGGREGATOR_ENABLED:
|
||||
return
|
||||
@ -26,6 +30,14 @@ class RSysLogHandler(logging.handlers.SysLogHandler):
|
||||
# unfortunately, we can't log that because...rsyslogd is down (and
|
||||
# would just us back ddown this code path)
|
||||
pass
|
||||
except BlockingIOError:
|
||||
# for <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
|
||||
|
||||
@ -740,7 +740,9 @@ class SAMLOrgAttrField(HybridDictField):
|
||||
class SAMLTeamAttrTeamOrgMapField(HybridDictField):
|
||||
|
||||
team = fields.CharField(required=True, allow_null=False)
|
||||
team_alias = fields.CharField(required=False, allow_null=True)
|
||||
organization = fields.CharField(required=True, allow_null=False)
|
||||
organization_alias = fields.CharField(required=False, allow_null=True)
|
||||
|
||||
child = _Forbidden()
|
||||
|
||||
|
||||
@ -187,13 +187,22 @@ def update_user_teams_by_saml_attr(backend, details, user=None, *args, **kwargs)
|
||||
|
||||
team_ids = []
|
||||
for team_name_map in team_map.get('team_org_map', []):
|
||||
team_name = team_name_map.get('team', '')
|
||||
team_name = team_name_map.get('team', None)
|
||||
team_alias = team_name_map.get('team_alias', None)
|
||||
organization_name = team_name_map.get('organization', None)
|
||||
organization_alias = team_name_map.get('organization_alias', None)
|
||||
if team_name in saml_team_names:
|
||||
if not team_name_map.get('organization', ''):
|
||||
if not organization_name:
|
||||
# Settings field validation should prevent this.
|
||||
logger.error("organization name invalid for team {}".format(team_name))
|
||||
continue
|
||||
org = Organization.objects.get_or_create(name=team_name_map['organization'])[0]
|
||||
|
||||
if organization_alias:
|
||||
organization_name = organization_alias
|
||||
org = Organization.objects.get_or_create(name=organization_name)[0]
|
||||
|
||||
if team_alias:
|
||||
team_name = team_alias
|
||||
team = Team.objects.get_or_create(name=team_name, organization=org)[0]
|
||||
|
||||
team_ids.append(team.id)
|
||||
|
||||
@ -193,6 +193,10 @@ class TestSAMLAttr():
|
||||
{'team': 'Red', 'organization': 'Default1'},
|
||||
{'team': 'Green', 'organization': 'Default1'},
|
||||
{'team': 'Green', 'organization': 'Default3'},
|
||||
{
|
||||
'team': 'Yellow', 'team_alias': 'Yellow_Alias',
|
||||
'organization': 'Default4', 'organization_alias': 'Default4_Alias'
|
||||
},
|
||||
]
|
||||
}
|
||||
return MockSettings()
|
||||
@ -285,3 +289,18 @@ class TestSAMLAttr():
|
||||
assert Team.objects.get(name='Green', organization__name='Default1').member_role.members.count() == 3
|
||||
assert Team.objects.get(name='Green', organization__name='Default3').member_role.members.count() == 3
|
||||
|
||||
def test_update_user_teams_alias_by_saml_attr(self, orgs, users, kwargs, mock_settings):
|
||||
with mock.patch('django.conf.settings', mock_settings):
|
||||
u1 = users[0]
|
||||
|
||||
# Test getting teams from attribute with team->org mapping
|
||||
kwargs['response']['attributes']['groups'] = ['Yellow']
|
||||
|
||||
# Ensure team and org will be created
|
||||
update_user_teams_by_saml_attr(None, None, u1, **kwargs)
|
||||
|
||||
assert Team.objects.filter(name='Yellow', organization__name='Default4').count() == 0
|
||||
assert Team.objects.filter(name='Yellow_Alias', organization__name='Default4_Alias').count() == 1
|
||||
assert Team.objects.get(
|
||||
name='Yellow_Alias', organization__name='Default4_Alias').member_role.members.count() == 1
|
||||
|
||||
|
||||
@ -71,6 +71,14 @@ class TestSAMLTeamAttrField():
|
||||
{'team': 'Engineering', 'organization': 'Ansible2'},
|
||||
{'team': 'Engineering2', 'organization': 'Ansible'},
|
||||
]},
|
||||
{'remove': True, 'saml_attr': 'foobar', 'team_org_map': [
|
||||
{
|
||||
'team': 'Engineering', 'team_alias': 'Engineering Team',
|
||||
'organization': 'Ansible', 'organization_alias': 'Awesome Org'
|
||||
},
|
||||
{'team': 'Engineering', 'organization': 'Ansible2'},
|
||||
{'team': 'Engineering2', 'organization': 'Ansible'},
|
||||
]},
|
||||
])
|
||||
def test_internal_value_valid(self, data):
|
||||
field = SAMLTeamAttrField()
|
||||
|
||||
6
awx/ui/package-lock.json
generated
6
awx/ui/package-lock.json
generated
@ -14435,9 +14435,9 @@
|
||||
}
|
||||
},
|
||||
"websocket-extensions": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
|
||||
"integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
|
||||
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
|
||||
"dev": true
|
||||
},
|
||||
"whet.extend": {
|
||||
|
||||
6
awx/ui_next/package-lock.json
generated
6
awx/ui_next/package-lock.json
generated
@ -16320,9 +16320,9 @@
|
||||
}
|
||||
},
|
||||
"websocket-extensions": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
|
||||
"integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg=="
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
|
||||
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="
|
||||
},
|
||||
"whatwg-encoding": {
|
||||
"version": "1.0.5",
|
||||
|
||||
@ -55,12 +55,12 @@ function App() {
|
||||
{getRouteConfig(i18n)
|
||||
.flatMap(({ routes }) => routes)
|
||||
.map(({ path, screen: Screen }) => (
|
||||
<ProtectedRoute auth key={path} path={path}>
|
||||
<ProtectedRoute key={path} path={path}>
|
||||
<Screen match={match} />
|
||||
</ProtectedRoute>
|
||||
))
|
||||
.concat(
|
||||
<ProtectedRoute auth key="not-found" path="*">
|
||||
<ProtectedRoute key="not-found" path="*">
|
||||
<NotFound />
|
||||
</ProtectedRoute>
|
||||
)}
|
||||
|
||||
@ -40,7 +40,13 @@ class ClipboardCopyButton extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { clickTip, entryDelay, exitDelay, hoverTip } = this.props;
|
||||
const {
|
||||
copyTip,
|
||||
entryDelay,
|
||||
exitDelay,
|
||||
copiedSuccessTip,
|
||||
isDisabled,
|
||||
} = this.props;
|
||||
const { copied } = this.state;
|
||||
|
||||
return (
|
||||
@ -48,12 +54,13 @@ class ClipboardCopyButton extends React.Component {
|
||||
entryDelay={entryDelay}
|
||||
exitDelay={exitDelay}
|
||||
trigger="mouseenter focus click"
|
||||
content={copied ? clickTip : hoverTip}
|
||||
content={copied ? copiedSuccessTip : copyTip}
|
||||
>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
variant="plain"
|
||||
onClick={this.handleCopyClick}
|
||||
aria-label={hoverTip}
|
||||
aria-label={copyTip}
|
||||
>
|
||||
<CopyIcon />
|
||||
</Button>
|
||||
@ -63,12 +70,13 @@ class ClipboardCopyButton extends React.Component {
|
||||
}
|
||||
|
||||
ClipboardCopyButton.propTypes = {
|
||||
clickTip: PropTypes.string.isRequired,
|
||||
copyTip: PropTypes.string.isRequired,
|
||||
entryDelay: PropTypes.number,
|
||||
exitDelay: PropTypes.number,
|
||||
hoverTip: PropTypes.string.isRequired,
|
||||
copiedSuccessTip: PropTypes.string.isRequired,
|
||||
stringToCopy: PropTypes.string.isRequired,
|
||||
switchDelay: PropTypes.number,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
ClipboardCopyButton.defaultProps = {
|
||||
|
||||
@ -13,6 +13,7 @@ describe('ClipboardCopyButton', () => {
|
||||
clickTip="foo"
|
||||
hoverTip="bar"
|
||||
stringToCopy="foobar!"
|
||||
isDisabled={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
@ -23,6 +24,7 @@ describe('ClipboardCopyButton', () => {
|
||||
clickTip="foo"
|
||||
hoverTip="bar"
|
||||
stringToCopy="foobar!"
|
||||
isDisabled={false}
|
||||
/>
|
||||
).find('ClipboardCopyButton');
|
||||
expect(wrapper.state('copied')).toBe(false);
|
||||
@ -33,4 +35,15 @@ describe('ClipboardCopyButton', () => {
|
||||
wrapper.update();
|
||||
expect(wrapper.state('copied')).toBe(false);
|
||||
});
|
||||
test('should render disabled button', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ClipboardCopyButton
|
||||
clickTip="foo"
|
||||
hoverTip="bar"
|
||||
stringToCopy="foobar!"
|
||||
isDisabled
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('Button').prop('isDisabled')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,11 +1,29 @@
|
||||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons';
|
||||
import { Tooltip } from '@patternfly/react-core';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import yaml from 'js-yaml';
|
||||
import PromptDetail from '../../PromptDetail';
|
||||
import mergeExtraVars, { maskPasswords } from '../mergeExtraVars';
|
||||
import getSurveyValues from '../getSurveyValues';
|
||||
import PromptDetail from '../../PromptDetail';
|
||||
|
||||
function PreviewStep({ resource, config, survey, formErrors }) {
|
||||
const ExclamationCircleIcon = styled(PFExclamationCircleIcon)`
|
||||
margin-left: 10px;
|
||||
margin-top: -2px;
|
||||
`;
|
||||
|
||||
const ErrorMessageWrapper = styled.div`
|
||||
align-items: center;
|
||||
color: var(--pf-global--danger-color--200);
|
||||
display: flex;
|
||||
font-weight: var(--pf-global--FontWeight--bold);
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
function PreviewStep({ resource, config, survey, formErrors, i18n }) {
|
||||
const { values } = useFormikContext();
|
||||
const surveyValues = getSurveyValues(values);
|
||||
|
||||
@ -29,21 +47,26 @@ function PreviewStep({ resource, config, survey, formErrors }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<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
|
||||
resource={resource}
|
||||
launchConfig={config}
|
||||
overrides={overrides}
|
||||
/>
|
||||
{formErrors && (
|
||||
<ul css="color: red">
|
||||
{Object.keys(formErrors).map(
|
||||
field => `${field}: ${formErrors[field]}`
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default PreviewStep;
|
||||
export default withI18n()(PreviewStep);
|
||||
|
||||
@ -24,6 +24,10 @@ const survey = {
|
||||
],
|
||||
};
|
||||
|
||||
const formErrors = {
|
||||
inventory: 'An inventory must be selected',
|
||||
};
|
||||
|
||||
describe('PreviewStep', () => {
|
||||
test('should render PromptDetail', async () => {
|
||||
let wrapper;
|
||||
@ -37,6 +41,7 @@ describe('PreviewStep', () => {
|
||||
survey_enabled: true,
|
||||
}}
|
||||
survey={survey}
|
||||
formErrors={formErrors}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
@ -62,6 +67,7 @@ describe('PreviewStep', () => {
|
||||
config={{
|
||||
ask_limit_on_launch: true,
|
||||
}}
|
||||
formErrors={formErrors}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
@ -85,6 +91,7 @@ describe('PreviewStep', () => {
|
||||
config={{
|
||||
ask_variables_on_launch: true,
|
||||
}}
|
||||
formErrors={formErrors}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
|
||||
@ -19,7 +19,8 @@ export default function useCredentialsStep(
|
||||
initialValues: getInitialValues(config, resource),
|
||||
validate,
|
||||
isReady: true,
|
||||
error: null,
|
||||
contentError: null,
|
||||
formError: null,
|
||||
setTouched: setFieldsTouched => {
|
||||
setFieldsTouched({
|
||||
credentials: true,
|
||||
|
||||
@ -27,7 +27,8 @@ export default function useInventoryStep(config, resource, visitedSteps, i18n) {
|
||||
initialValues: getInitialValues(config, resource),
|
||||
validate,
|
||||
isReady: true,
|
||||
error: null,
|
||||
contentError: null,
|
||||
formError: stepErrors,
|
||||
setTouched: setFieldsTouched => {
|
||||
setFieldsTouched({
|
||||
inventory: true,
|
||||
|
||||
@ -24,7 +24,8 @@ export default function useOtherPrompt(config, resource, visitedSteps, i18n) {
|
||||
initialValues: getInitialValues(config, resource),
|
||||
validate,
|
||||
isReady: true,
|
||||
error: null,
|
||||
contentError: null,
|
||||
formError: stepErrors,
|
||||
setTouched: setFieldsTouched => {
|
||||
setFieldsTouched({
|
||||
job_type: true,
|
||||
|
||||
@ -54,7 +54,8 @@ export default function useSurveyStep(config, resource, visitedSteps, i18n) {
|
||||
validate,
|
||||
survey,
|
||||
isReady: !isLoading && !!survey,
|
||||
error,
|
||||
contentError: error,
|
||||
formError: stepErrors,
|
||||
setTouched: setFieldsTouched => {
|
||||
if (!survey || !survey.spec) {
|
||||
return;
|
||||
|
||||
@ -13,14 +13,13 @@ export default function useSteps(config, resource, i18n) {
|
||||
useOtherPromptsStep(config, resource, visited, i18n),
|
||||
useSurveyStep(config, resource, visited, i18n),
|
||||
];
|
||||
|
||||
const formErrorsContent = steps
|
||||
.filter(s => s?.formError && Object.keys(s.formError).length > 0)
|
||||
.map(({ formError }) => formError);
|
||||
|
||||
steps.push(
|
||||
usePreviewStep(
|
||||
config,
|
||||
resource,
|
||||
steps[3].survey,
|
||||
{}, // TODO: formErrors ?
|
||||
i18n
|
||||
)
|
||||
usePreviewStep(config, resource, steps[3].survey, formErrorsContent, i18n)
|
||||
);
|
||||
|
||||
const pfSteps = steps.map(s => s.step).filter(s => s != null);
|
||||
@ -31,8 +30,9 @@ export default function useSteps(config, resource, i18n) {
|
||||
};
|
||||
}, {});
|
||||
const isReady = !steps.some(s => !s.isReady);
|
||||
const stepWithError = steps.find(s => s.error);
|
||||
const contentError = stepWithError ? stepWithError.error : null;
|
||||
|
||||
const stepWithError = steps.find(s => s.contentError);
|
||||
const contentError = stepWithError ? stepWithError.contentError : null;
|
||||
|
||||
const validate = values => {
|
||||
const errors = steps.reduce((acc, cur) => {
|
||||
|
||||
@ -138,7 +138,7 @@ function getRouteConfig(i18n) {
|
||||
screen: InstanceGroups,
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Integrations`),
|
||||
title: i18n._(t`Applications`),
|
||||
path: '/applications',
|
||||
screen: Applications,
|
||||
},
|
||||
|
||||
@ -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;
|
||||
1
awx/ui_next/src/screens/Application/Application/index.js
Normal file
1
awx/ui_next/src/screens/Application/Application/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Application';
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './ApplicationAdd';
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './ApplicationDetails';
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './ApplicationEdit';
|
||||
@ -1,26 +1,49 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
Title,
|
||||
} from '@patternfly/react-core';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
class Applications extends Component {
|
||||
render() {
|
||||
const { i18n } = this.props;
|
||||
const { light } = PageSectionVariants;
|
||||
import ApplicationsList from './ApplicationsList';
|
||||
import ApplicationAdd from './ApplicationAdd';
|
||||
import Application from './Application';
|
||||
import Breadcrumbs from '../../components/Breadcrumbs';
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageSection variant={light} className="pf-m-condensed">
|
||||
<Title size="2xl">{i18n._(t`Applications`)}</Title>
|
||||
</PageSection>
|
||||
<PageSection />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
function Applications({ i18n }) {
|
||||
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
||||
'/applications': i18n._(t`Applications`),
|
||||
'/applications/add': i18n._(t`Create New Application`),
|
||||
});
|
||||
|
||||
const buildBreadcrumbConfig = useCallback(
|
||||
application => {
|
||||
if (!application) {
|
||||
return;
|
||||
}
|
||||
|
||||
setBreadcrumbConfig({
|
||||
'/applications': i18n._(t`Applications`),
|
||||
'/applications/add': i18n._(t`Create New Application`),
|
||||
[`/application/${application.id}`]: `${application.name}`,
|
||||
});
|
||||
},
|
||||
[i18n]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<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);
|
||||
|
||||
@ -7,12 +7,10 @@ import Applications from './Applications';
|
||||
describe('<Applications />', () => {
|
||||
let pageWrapper;
|
||||
let pageSections;
|
||||
let title;
|
||||
|
||||
beforeEach(() => {
|
||||
pageWrapper = mountWithContexts(<Applications />);
|
||||
pageSections = pageWrapper.find('PageSection');
|
||||
title = pageWrapper.find('Title');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -21,9 +19,7 @@ describe('<Applications />', () => {
|
||||
|
||||
test('initially renders without crashing', () => {
|
||||
expect(pageWrapper.length).toBe(1);
|
||||
expect(pageSections.length).toBe(2);
|
||||
expect(title.length).toBe(1);
|
||||
expect(title.props().size).toBe('2xl');
|
||||
expect(pageSections.length).toBe(1);
|
||||
expect(pageSections.first().props().variant).toBe('light');
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './ApplicationsList';
|
||||
@ -32,6 +32,11 @@ const DataListAction = styled(_DataListAction)`
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: repeat(3, 40px);
|
||||
`;
|
||||
|
||||
const Label = styled.span`
|
||||
color: var(--pf-global--disabled-color--100);
|
||||
`;
|
||||
|
||||
function ProjectListItem({
|
||||
project,
|
||||
isSelected,
|
||||
@ -121,13 +126,17 @@ function ProjectListItem({
|
||||
</DataListCell>,
|
||||
<DataListCell key="revision">
|
||||
{project.scm_revision.substring(0, 7)}
|
||||
{project.scm_revision ? (
|
||||
<ClipboardCopyButton
|
||||
stringToCopy={project.scm_revision}
|
||||
hoverTip={i18n._(t`Copy full revision to clipboard.`)}
|
||||
clickTip={i18n._(t`Successfully copied to clipboard!`)}
|
||||
/>
|
||||
) : null}
|
||||
{!project.scm_revision && (
|
||||
<Label aria-label={i18n._(t`copy to clipboard disabled`)}>
|
||||
{i18n._(t`Sync for revision`)}
|
||||
</Label>
|
||||
)}
|
||||
<ClipboardCopyButton
|
||||
isDisabled={!project.scm_revision}
|
||||
stringToCopy={project.scm_revision}
|
||||
copyTip={i18n._(t`Copy full revision to clipboard.`)}
|
||||
copiedSuccessTip={i18n._(t`Successfully copied to clipboard!`)}
|
||||
/>
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
|
||||
@ -218,4 +218,34 @@ describe('<ProjectsListItem />', () => {
|
||||
);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withFormik, useField, useFormikContext } from 'formik';
|
||||
import { withFormik, useField } from 'formik';
|
||||
import {
|
||||
Form,
|
||||
FormGroup,
|
||||
@ -52,8 +52,6 @@ function JobTemplateForm({
|
||||
submitError,
|
||||
i18n,
|
||||
}) {
|
||||
const { values: formikValues } = useFormikContext();
|
||||
|
||||
const [contentError, setContentError] = useState(false);
|
||||
const [inventory, setInventory] = useState(
|
||||
template?.summary_fields?.inventory
|
||||
@ -65,6 +63,7 @@ function JobTemplateForm({
|
||||
Boolean(template.webhook_service)
|
||||
);
|
||||
|
||||
const [askInventoryOnLaunchField] = useField('ask_inventory_on_launch');
|
||||
const [jobTypeField, jobTypeMeta, jobTypeHelpers] = useField({
|
||||
name: 'job_type',
|
||||
validate: required(null, i18n),
|
||||
@ -81,7 +80,7 @@ function JobTemplateForm({
|
||||
});
|
||||
const [credentialField, , credentialHelpers] = useField('credentials');
|
||||
const [labelsField, , labelsHelpers] = useField('labels');
|
||||
const [limitField, limitMeta] = useField('limit');
|
||||
const [limitField, limitMeta, limitHelpers] = useField('limit');
|
||||
const [verbosityField] = useField('verbosity');
|
||||
const [diffModeField, , diffModeHelpers] = useField('diff_mode');
|
||||
const [instanceGroupsField, , instanceGroupsHelpers] = useField(
|
||||
@ -231,7 +230,7 @@ function JobTemplateForm({
|
||||
</FieldWithPrompt>
|
||||
<FieldWithPrompt
|
||||
fieldId="template-inventory"
|
||||
isRequired={!formikValues.ask_inventory_on_launch}
|
||||
isRequired={!askInventoryOnLaunchField.value}
|
||||
label={i18n._(t`Inventory`)}
|
||||
promptId="template-ask-inventory-on-launch"
|
||||
promptName="ask_inventory_on_launch"
|
||||
@ -245,11 +244,11 @@ function JobTemplateForm({
|
||||
inventoryHelpers.setValue(value ? value.id : null);
|
||||
setInventory(value);
|
||||
}}
|
||||
required={!formikValues.ask_inventory_on_launch}
|
||||
required={!askInventoryOnLaunchField.value}
|
||||
touched={inventoryMeta.touched}
|
||||
error={inventoryMeta.error}
|
||||
/>
|
||||
{(inventoryMeta.touched || formikValues.ask_inventory_on_launch) &&
|
||||
{(inventoryMeta.touched || askInventoryOnLaunchField.value) &&
|
||||
inventoryMeta.error && (
|
||||
<div
|
||||
className="pf-c-form__helper-text pf-m-error"
|
||||
@ -283,8 +282,8 @@ function JobTemplateForm({
|
||||
<TextInput
|
||||
id="template-scm-branch"
|
||||
{...scmField}
|
||||
onChange={(value, event) => {
|
||||
scmField.onChange(event);
|
||||
onChange={value => {
|
||||
scmHelpers.setValue(value);
|
||||
}}
|
||||
/>
|
||||
</FieldWithPrompt>
|
||||
@ -383,8 +382,8 @@ function JobTemplateForm({
|
||||
id="template-limit"
|
||||
{...limitField}
|
||||
isValid={!limitMeta.touched || !limitMeta.error}
|
||||
onChange={(value, event) => {
|
||||
limitField.onChange(event);
|
||||
onChange={value => {
|
||||
limitHelpers.setValue(value);
|
||||
}}
|
||||
/>
|
||||
</FieldWithPrompt>
|
||||
|
||||
@ -29,6 +29,7 @@ describe('<JobTemplateForm />', () => {
|
||||
playbook: 'Baz',
|
||||
type: 'job_template',
|
||||
scm_branch: 'Foo',
|
||||
limit: '5000',
|
||||
summary_fields: {
|
||||
inventory: {
|
||||
id: 2,
|
||||
@ -184,9 +185,10 @@ describe('<JobTemplateForm />', () => {
|
||||
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('input#template-scm-branch').simulate('change', {
|
||||
target: { value: 'devel', name: 'scm_branch' },
|
||||
});
|
||||
wrapper.find('TextInputBase#template-scm-branch').prop('onChange')(
|
||||
'devel'
|
||||
);
|
||||
wrapper.find('TextInputBase#template-limit').prop('onChange')(1234567890);
|
||||
wrapper.find('AnsibleSelect[name="playbook"]').simulate('change', {
|
||||
target: { value: 'new baz type', name: 'playbook' },
|
||||
});
|
||||
@ -221,6 +223,9 @@ describe('<JobTemplateForm />', () => {
|
||||
expect(wrapper.find('input#template-scm-branch').prop('value')).toEqual(
|
||||
'devel'
|
||||
);
|
||||
expect(wrapper.find('input#template-limit').prop('value')).toEqual(
|
||||
1234567890
|
||||
);
|
||||
expect(
|
||||
wrapper.find('AnsibleSelect[name="playbook"]').prop('value')
|
||||
).toEqual('new baz type');
|
||||
|
||||
@ -31,8 +31,11 @@ options:
|
||||
tower_oauthtoken:
|
||||
description:
|
||||
- The Tower OAuth token to use.
|
||||
- This value can be in one of two formats.
|
||||
- A string which is the token itself. (i.e. bqV5txm97wqJqtkxlMkhQz0pKhRMMX)
|
||||
- A dictionary structure as returned by the tower_token module.
|
||||
- If value not set, will try environment variable C(TOWER_OAUTH_TOKEN) and then config files
|
||||
type: str
|
||||
type: raw
|
||||
version_added: "3.7"
|
||||
validate_certs:
|
||||
description:
|
||||
|
||||
@ -12,7 +12,6 @@ DOCUMENTATION = '''
|
||||
- Matthew Jones (@matburt)
|
||||
- Yunfan Zhang (@YunfanZhang42)
|
||||
short_description: Ansible dynamic inventory plugin for Ansible Tower.
|
||||
version_added: "2.7"
|
||||
description:
|
||||
- Reads inventories from Ansible Tower.
|
||||
- Supports reading configuration from both YAML config file and environment variables.
|
||||
@ -21,31 +20,23 @@ DOCUMENTATION = '''
|
||||
are missing, this plugin will try to fill in missing arguments by reading from environment variables.
|
||||
- If reading configurations from environment variables, the path in the command must be @tower_inventory.
|
||||
options:
|
||||
plugin:
|
||||
description: the name of this plugin, it should always be set to 'tower'
|
||||
for this plugin to recognize it as it's own.
|
||||
env:
|
||||
- name: ANSIBLE_INVENTORY_ENABLED
|
||||
required: True
|
||||
choices: ['tower']
|
||||
host:
|
||||
description: The network address of your Ansible Tower host.
|
||||
type: string
|
||||
env:
|
||||
- name: TOWER_HOST
|
||||
required: True
|
||||
username:
|
||||
description: The user that you plan to use to access inventories on Ansible Tower.
|
||||
type: string
|
||||
env:
|
||||
- name: TOWER_USERNAME
|
||||
required: True
|
||||
password:
|
||||
description: The password for your Ansible Tower user.
|
||||
type: string
|
||||
env:
|
||||
- name: TOWER_PASSWORD
|
||||
required: True
|
||||
oauth_token:
|
||||
description:
|
||||
- The Tower OAuth token to use.
|
||||
env:
|
||||
- name: TOWER_OAUTH_TOKEN
|
||||
inventory_id:
|
||||
description:
|
||||
- The ID of the Ansible Tower inventory that you wish to import.
|
||||
@ -56,19 +47,18 @@ DOCUMENTATION = '''
|
||||
env:
|
||||
- name: TOWER_INVENTORY
|
||||
required: True
|
||||
validate_certs:
|
||||
description: Specify whether Ansible should verify the SSL certificate of Ansible Tower host.
|
||||
verify_ssl:
|
||||
description:
|
||||
- Specify whether Ansible should verify the SSL certificate of Ansible Tower host.
|
||||
- Defaults to True, but this is handled by the shared module_utils code
|
||||
type: bool
|
||||
default: True
|
||||
env:
|
||||
- name: TOWER_VERIFY_SSL
|
||||
required: False
|
||||
aliases: [ verify_ssl ]
|
||||
aliases: [ validate_certs ]
|
||||
include_metadata:
|
||||
description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host.
|
||||
type: bool
|
||||
default: False
|
||||
version_added: "2.8"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
@ -99,7 +89,6 @@ inventory_id: the_ID_of_targeted_ansible_tower_inventory
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from ansible.module_utils import six
|
||||
from ansible.module_utils._text import to_text, to_native
|
||||
@ -107,13 +96,11 @@ from ansible.errors import AnsibleParserError, AnsibleOptionsError
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin
|
||||
from ansible.config.manager import ensure_type
|
||||
|
||||
from ..module_utils.ansible_tower import make_request, CollectionsParserError, Request
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
|
||||
# Python 2/3 Compatibility
|
||||
try:
|
||||
from urlparse import urljoin
|
||||
except ImportError:
|
||||
from urllib.parse import urljoin
|
||||
|
||||
def handle_error(**kwargs):
|
||||
raise AnsibleParserError(to_native(kwargs.get('msg')))
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin):
|
||||
@ -131,20 +118,25 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
else:
|
||||
return False
|
||||
|
||||
def warn_callback(self, warning):
|
||||
self.display.warning(warning)
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
super(InventoryModule, self).parse(inventory, loader, path)
|
||||
if not self.no_config_file_supplied and os.path.isfile(path):
|
||||
self._read_config_data(path)
|
||||
# Read inventory from tower server.
|
||||
# Note the environment variables will be handled automatically by InventoryManager.
|
||||
tower_host = self.get_option('host')
|
||||
if not re.match('(?:http|https)://', tower_host):
|
||||
tower_host = 'https://{tower_host}'.format(tower_host=tower_host)
|
||||
|
||||
request_handler = Request(url_username=self.get_option('username'),
|
||||
url_password=self.get_option('password'),
|
||||
force_basic_auth=True,
|
||||
validate_certs=self.get_option('validate_certs'))
|
||||
# Defer processing of params to logic shared with the modules
|
||||
module_params = {}
|
||||
for plugin_param, module_param in TowerModule.short_params.items():
|
||||
opt_val = self.get_option(plugin_param)
|
||||
if opt_val is not None:
|
||||
module_params[module_param] = opt_val
|
||||
|
||||
module = TowerModule(
|
||||
argument_spec={}, direct_params=module_params,
|
||||
error_callback=handle_error, warn_callback=self.warn_callback
|
||||
)
|
||||
|
||||
# validate type of inventory_id because we allow two types as special case
|
||||
inventory_id = self.get_option('inventory_id')
|
||||
@ -159,13 +151,11 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
'not integer, and cannot convert to string: {err}'.format(err=to_native(e))
|
||||
)
|
||||
inventory_id = inventory_id.replace('/', '')
|
||||
inventory_url = '/api/v2/inventories/{inv_id}/script/?hostvars=1&towervars=1&all=1'.format(inv_id=inventory_id)
|
||||
inventory_url = urljoin(tower_host, inventory_url)
|
||||
inventory_url = '/api/v2/inventories/{inv_id}/script/'.format(inv_id=inventory_id)
|
||||
|
||||
try:
|
||||
inventory = make_request(request_handler, inventory_url)
|
||||
except CollectionsParserError as e:
|
||||
raise AnsibleParserError(to_native(e))
|
||||
inventory = module.get_endpoint(
|
||||
inventory_url, data={'hostvars': '1', 'towervars': '1', 'all': '1'}
|
||||
)['json']
|
||||
|
||||
# To start with, create all the groups.
|
||||
for group_name in inventory:
|
||||
@ -195,12 +185,8 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
|
||||
# Fetch extra variables if told to do so
|
||||
if self.get_option('include_metadata'):
|
||||
config_url = urljoin(tower_host, '/api/v2/config/')
|
||||
|
||||
try:
|
||||
config_data = make_request(request_handler, config_url)
|
||||
except CollectionsParserError as e:
|
||||
raise AnsibleParserError(to_native(e))
|
||||
config_data = module.get_endpoint('/api/v2/config/')['json']
|
||||
|
||||
server_data = {}
|
||||
server_data['license_type'] = config_data.get('license_info', {}).get('license_type', 'unknown')
|
||||
|
||||
@ -6,7 +6,6 @@ __metaclass__ = type
|
||||
DOCUMENTATION = """
|
||||
lookup: tower_schedule_rrule
|
||||
author: John Westcott IV (@john-westcott-iv)
|
||||
version_added: "3.7"
|
||||
short_description: Generate an rrule string which can be used for Tower Schedules
|
||||
requirements:
|
||||
- pytz
|
||||
|
||||
@ -29,14 +29,9 @@
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.urls import urllib_error, ConnectionError, socket, httplib
|
||||
from ansible.module_utils.urls import Request # noqa
|
||||
|
||||
TOWER_CLI_IMP_ERR = None
|
||||
try:
|
||||
import tower_cli.utils.exceptions as exc
|
||||
@ -51,31 +46,6 @@ except ImportError:
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
|
||||
|
||||
class CollectionsParserError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def make_request(request_handler, tower_url):
|
||||
'''
|
||||
Makes the request to given URL, handles errors, returns JSON
|
||||
'''
|
||||
try:
|
||||
response = request_handler.get(tower_url)
|
||||
except (ConnectionError, urllib_error.URLError, socket.error, httplib.HTTPException) as e:
|
||||
n_error_msg = 'Connection to remote host failed: {err}'.format(err=to_native(e))
|
||||
# If Tower gives a readable error message, display that message to the user.
|
||||
if callable(getattr(e, 'read', None)):
|
||||
n_error_msg += ' with message: {err_msg}'.format(err_msg=to_native(e.read()))
|
||||
raise CollectionsParserError(n_error_msg)
|
||||
|
||||
# Attempt to parse JSON.
|
||||
try:
|
||||
return json.loads(response.read())
|
||||
except (ValueError, TypeError) as e:
|
||||
# If the JSON parse fails, print the ValueError
|
||||
raise CollectionsParserError('Failed to parse json from host: {err}'.format(err=to_native(e)))
|
||||
|
||||
|
||||
def tower_auth_config(module):
|
||||
'''
|
||||
`tower_auth_config` attempts to load the tower-cli.cfg file
|
||||
|
||||
@ -3,7 +3,7 @@ __metaclass__ = type
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||
from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError
|
||||
from ansible.module_utils.six import PY2
|
||||
from ansible.module_utils.six import PY2, string_types
|
||||
from ansible.module_utils.six.moves import StringIO
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode
|
||||
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
||||
@ -42,7 +42,21 @@ class TowerModule(AnsibleModule):
|
||||
'tower': 'Red Hat Ansible Tower',
|
||||
}
|
||||
url = None
|
||||
honorred_settings = ('host', 'username', 'password', 'verify_ssl', 'oauth_token')
|
||||
AUTH_ARGSPEC = dict(
|
||||
tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])),
|
||||
tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])),
|
||||
tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])),
|
||||
validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])),
|
||||
tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])),
|
||||
tower_config_file=dict(type='path', required=False, default=None),
|
||||
)
|
||||
short_params = {
|
||||
'host': 'tower_host',
|
||||
'username': 'tower_username',
|
||||
'password': 'tower_password',
|
||||
'verify_ssl': 'validate_certs',
|
||||
'oauth_token': 'tower_oauthtoken',
|
||||
}
|
||||
host = '127.0.0.1'
|
||||
username = None
|
||||
password = None
|
||||
@ -55,36 +69,46 @@ class TowerModule(AnsibleModule):
|
||||
config_name = 'tower_cli.cfg'
|
||||
ENCRYPTED_STRING = "$encrypted$"
|
||||
version_checked = False
|
||||
error_callback = None
|
||||
warn_callback = None
|
||||
|
||||
def __init__(self, argument_spec, **kwargs):
|
||||
args = dict(
|
||||
tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])),
|
||||
tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])),
|
||||
tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])),
|
||||
validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])),
|
||||
tower_oauthtoken=dict(type='str', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])),
|
||||
tower_config_file=dict(type='path', required=False, default=None),
|
||||
)
|
||||
args.update(argument_spec)
|
||||
def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs):
|
||||
full_argspec = {}
|
||||
full_argspec.update(TowerModule.AUTH_ARGSPEC)
|
||||
full_argspec.update(argument_spec)
|
||||
kwargs['supports_check_mode'] = True
|
||||
|
||||
self.error_callback = error_callback
|
||||
self.warn_callback = warn_callback
|
||||
|
||||
self.json_output = {'changed': False}
|
||||
|
||||
super(TowerModule, self).__init__(argument_spec=args, **kwargs)
|
||||
if direct_params is not None:
|
||||
self.params = direct_params
|
||||
else:
|
||||
super(TowerModule, self).__init__(argument_spec=full_argspec, **kwargs)
|
||||
|
||||
self.load_config_files()
|
||||
|
||||
# Parameters specified on command line will override settings in any config
|
||||
if self.params.get('tower_host'):
|
||||
self.host = self.params.get('tower_host')
|
||||
if self.params.get('tower_username'):
|
||||
self.username = self.params.get('tower_username')
|
||||
if self.params.get('tower_password'):
|
||||
self.password = self.params.get('tower_password')
|
||||
if self.params.get('validate_certs') is not None:
|
||||
self.verify_ssl = self.params.get('validate_certs')
|
||||
for short_param, long_param in self.short_params.items():
|
||||
direct_value = self.params.get(long_param)
|
||||
if direct_value is not None:
|
||||
setattr(self, short_param, direct_value)
|
||||
|
||||
# Perform magic depending on whether tower_oauthtoken is a string or a dict
|
||||
if self.params.get('tower_oauthtoken'):
|
||||
self.oauth_token = self.params.get('tower_oauthtoken')
|
||||
token_param = self.params.get('tower_oauthtoken')
|
||||
if type(token_param) is dict:
|
||||
if 'token' in token_param:
|
||||
self.oauth_token = self.params.get('tower_oauthtoken')['token']
|
||||
else:
|
||||
self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry")
|
||||
elif isinstance(token_param, string_types):
|
||||
self.oauth_token = self.params.get('tower_oauthtoken')
|
||||
else:
|
||||
error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__)
|
||||
self.fail_json(msg=error_msg)
|
||||
|
||||
# Perform some basic validation
|
||||
if not re.match('^https{0,1}://', self.host):
|
||||
@ -116,10 +140,10 @@ class TowerModule(AnsibleModule):
|
||||
|
||||
# If we have a specified tower config, load it
|
||||
if self.params.get('tower_config_file'):
|
||||
duplicated_params = []
|
||||
for direct_field in ('tower_host', 'tower_username', 'tower_password', 'validate_certs', 'tower_oauthtoken'):
|
||||
if self.params.get(direct_field):
|
||||
duplicated_params.append(direct_field)
|
||||
duplicated_params = [
|
||||
fn for fn in self.AUTH_ARGSPEC
|
||||
if fn != 'tower_config_file' and self.params.get(fn) is not None
|
||||
]
|
||||
if duplicated_params:
|
||||
self.warn((
|
||||
'The parameter(s) {0} were provided at the same time as tower_config_file. '
|
||||
@ -184,7 +208,7 @@ class TowerModule(AnsibleModule):
|
||||
|
||||
# If we made it here then we have values from reading the ini file, so let's pull them out into a dict
|
||||
config_data = {}
|
||||
for honorred_setting in self.honorred_settings:
|
||||
for honorred_setting in self.short_params:
|
||||
try:
|
||||
config_data[honorred_setting] = config.get('general', honorred_setting)
|
||||
except NoOptionError:
|
||||
@ -197,7 +221,7 @@ class TowerModule(AnsibleModule):
|
||||
raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e))
|
||||
|
||||
# If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here
|
||||
for honorred_setting in self.honorred_settings:
|
||||
for honorred_setting in self.short_params:
|
||||
if honorred_setting in config_data:
|
||||
# Veriffy SSL must be a boolean
|
||||
if honorred_setting == 'verify_ssl':
|
||||
@ -494,6 +518,9 @@ class TowerModule(AnsibleModule):
|
||||
item_name = existing_item['username']
|
||||
elif 'identifier' in existing_item:
|
||||
item_name = existing_item['identifier']
|
||||
elif item_type == 'o_auth2_access_token':
|
||||
# An oauth2 token has no name, instead we will use its id for any of the messages
|
||||
item_name = existing_item['id']
|
||||
else:
|
||||
self.fail_json(msg="Unable to process delete of {0} due to missing name".format(item_type))
|
||||
|
||||
@ -748,13 +775,22 @@ class TowerModule(AnsibleModule):
|
||||
def fail_json(self, **kwargs):
|
||||
# Try to log out if we are authenticated
|
||||
self.logout()
|
||||
super(TowerModule, self).fail_json(**kwargs)
|
||||
if self.error_callback:
|
||||
self.error_callback(**kwargs)
|
||||
else:
|
||||
super(TowerModule, self).fail_json(**kwargs)
|
||||
|
||||
def exit_json(self, **kwargs):
|
||||
# Try to log out if we are authenticated
|
||||
self.logout()
|
||||
super(TowerModule, self).exit_json(**kwargs)
|
||||
|
||||
def warn(self, warning):
|
||||
if self.warn_callback is not None:
|
||||
self.warn_callback(warning)
|
||||
else:
|
||||
super(TowerModule, self).warn(warning)
|
||||
|
||||
def is_job_done(self, job_status):
|
||||
if job_status in ['new', 'pending', 'waiting', 'running']:
|
||||
return False
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_credential
|
||||
author: "Wayne Witzel III (@wwitzel3)"
|
||||
version_added: "2.3"
|
||||
short_description: create, update, or destroy Ansible Tower credential.
|
||||
description:
|
||||
- Create, update, or destroy Ansible Tower credentials. See
|
||||
@ -45,7 +44,6 @@ options:
|
||||
description:
|
||||
- Name of credential type.
|
||||
- Will be preferred over kind
|
||||
version_added: "2.10"
|
||||
type: str
|
||||
inputs:
|
||||
description:
|
||||
@ -53,7 +51,6 @@ options:
|
||||
Credential inputs where the keys are var names used in templating.
|
||||
Refer to the Ansible Tower documentation for example syntax.
|
||||
- Any fields in this dict will take prescedence over any fields mentioned below (i.e. host, username, etc)
|
||||
version_added: "2.9"
|
||||
type: dict
|
||||
user:
|
||||
description:
|
||||
@ -124,7 +121,6 @@ options:
|
||||
description:
|
||||
- STS token for aws type.
|
||||
- Deprecated, please use inputs
|
||||
version_added: "2.6"
|
||||
type: str
|
||||
secret:
|
||||
description:
|
||||
@ -177,7 +173,6 @@ options:
|
||||
- This parameter is only valid if C(kind) is specified as C(vault).
|
||||
- Deprecated, please use inputs
|
||||
type: str
|
||||
version_added: "2.8"
|
||||
state:
|
||||
description:
|
||||
- Desired state of the resource.
|
||||
@ -360,9 +355,9 @@ def main():
|
||||
# Deprication warnings
|
||||
for legacy_input in OLD_INPUT_NAMES:
|
||||
if module.params.get(legacy_input) is not None:
|
||||
module.deprecate(msg='{0} parameter has been deprecated, please use inputs instead'.format(legacy_input), version="3.6")
|
||||
module.deprecate(msg='{0} parameter has been deprecated, please use inputs instead'.format(legacy_input), version="ansible.tower:4.0.0")
|
||||
if kind:
|
||||
module.deprecate(msg='The kind parameter has been deprecated, please use credential_type instead', version="3.6")
|
||||
module.deprecate(msg='The kind parameter has been deprecated, please use credential_type instead', version="ansible.tower:4.0.0")
|
||||
|
||||
cred_type_id = module.resolve_name_to_id('credential_types', credential_type if credential_type else KIND_CHOICES[kind])
|
||||
if organization:
|
||||
|
||||
@ -18,7 +18,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_credential_type
|
||||
author: "Adrien Fleury (@fleu42)"
|
||||
version_added: "2.7"
|
||||
short_description: Create, update, or destroy custom Ansible Tower credential type.
|
||||
description:
|
||||
- Create, update, or destroy Ansible Tower credential type. See
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_group
|
||||
author: "Wayne Witzel III (@wwitzel3)"
|
||||
version_added: "2.3"
|
||||
short_description: create, update, or destroy Ansible Tower group.
|
||||
description:
|
||||
- Create, update, or destroy Ansible Tower groups. See
|
||||
@ -63,7 +62,6 @@ options:
|
||||
description:
|
||||
- A new name for this group (for renaming)
|
||||
type: str
|
||||
version_added: "3.7"
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
|
||||
@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_host
|
||||
version_added: "2.3"
|
||||
author: "Wayne Witzel III (@wwitzel3)"
|
||||
short_description: create, update, or destroy Ansible Tower host.
|
||||
description:
|
||||
@ -32,7 +31,6 @@ options:
|
||||
description:
|
||||
- To use when changing a hosts's name.
|
||||
type: str
|
||||
version_added: "3.7"
|
||||
description:
|
||||
description:
|
||||
- The description to use for the host.
|
||||
|
||||
@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_inventory
|
||||
version_added: "2.3"
|
||||
author: "Wayne Witzel III (@wwitzel3)"
|
||||
short_description: create, update, or destroy Ansible Tower inventory.
|
||||
description:
|
||||
@ -46,12 +45,10 @@ options:
|
||||
- The kind field. Cannot be modified after created.
|
||||
default: ""
|
||||
choices: ["", "smart"]
|
||||
version_added: "2.7"
|
||||
type: str
|
||||
host_filter:
|
||||
description:
|
||||
- The host_filter field. Only useful when C(kind=smart).
|
||||
version_added: "2.7"
|
||||
type: str
|
||||
state:
|
||||
description:
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_inventory_source
|
||||
author: "Adrien Fleury (@fleu42)"
|
||||
version_added: "2.7"
|
||||
short_description: create, update, or destroy Ansible Tower inventory source.
|
||||
description:
|
||||
- Create, update, or destroy Ansible Tower inventory source. See
|
||||
@ -32,7 +31,6 @@ options:
|
||||
description:
|
||||
- A new name for this assets (will rename the asset)
|
||||
type: str
|
||||
version_added: "3.7"
|
||||
description:
|
||||
description:
|
||||
- The description to use for the inventory source.
|
||||
@ -85,7 +83,6 @@ options:
|
||||
- Override vars in child groups and hosts with those from external source.
|
||||
type: bool
|
||||
custom_virtualenv:
|
||||
version_added: "2.9"
|
||||
description:
|
||||
- Local absolute file path containing a custom Python virtualenv to use.
|
||||
type: str
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_job_cancel
|
||||
author: "Wayne Witzel III (@wwitzel3)"
|
||||
version_added: "2.3"
|
||||
short_description: Cancel an Ansible Tower Job.
|
||||
description:
|
||||
- Cancel Ansible Tower jobs. See
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_job_launch
|
||||
author: "Wayne Witzel III (@wwitzel3)"
|
||||
version_added: "2.3"
|
||||
short_description: Launch an Ansible Job.
|
||||
description:
|
||||
- Launch an Ansible Tower jobs. See
|
||||
@ -64,29 +63,24 @@ options:
|
||||
- A specific of the SCM project to run the template on.
|
||||
- This is only applicable if your project allows for branch override.
|
||||
type: str
|
||||
version_added: "3.7"
|
||||
skip_tags:
|
||||
description:
|
||||
- Specific tags to skip from the playbook.
|
||||
type: list
|
||||
elements: str
|
||||
version_added: "3.7"
|
||||
verbosity:
|
||||
description:
|
||||
- Verbosity level for this job run
|
||||
type: int
|
||||
choices: [ 0, 1, 2, 3, 4, 5 ]
|
||||
version_added: "3.7"
|
||||
diff_mode:
|
||||
description:
|
||||
- Show the changes made by Ansible tasks where supported
|
||||
type: bool
|
||||
version_added: "3.7"
|
||||
credential_passwords:
|
||||
description:
|
||||
- Passwords for credentials which are set to prompt on launch
|
||||
type: dict
|
||||
version_added: "3.7"
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_job_list
|
||||
author: "Wayne Witzel III (@wwitzel3)"
|
||||
version_added: "2.3"
|
||||
short_description: List Ansible Tower jobs.
|
||||
description:
|
||||
- List Ansible Tower jobs. See
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_job_template
|
||||
author: "Wayne Witzel III (@wwitzel3)"
|
||||
version_added: "2.3"
|
||||
short_description: create, update, or destroy Ansible Tower job templates.
|
||||
description:
|
||||
- Create, update, or destroy Ansible Tower job templates. See
|
||||
@ -45,6 +44,14 @@ options:
|
||||
description:
|
||||
- Name of the inventory to use for the job template.
|
||||
type: str
|
||||
organization:
|
||||
description:
|
||||
- Organization the job template exists in.
|
||||
- Used to help lookup the object, cannot be modified using this module.
|
||||
- The Organization is inferred from the associated project
|
||||
- If not provided, will lookup by name only, which does not work with duplicates.
|
||||
- Requires Tower Version 3.7.0 or AWX 10.0.0 IS NOT backwards compatible with earlier versions.
|
||||
type: str
|
||||
project:
|
||||
description:
|
||||
- Name of the project to use for the job template.
|
||||
@ -57,19 +64,16 @@ options:
|
||||
description:
|
||||
- Name of the credential to use for the job template.
|
||||
- Deprecated, use 'credentials'.
|
||||
version_added: 2.7
|
||||
type: str
|
||||
credentials:
|
||||
description:
|
||||
- List of credentials to use for the job template.
|
||||
type: list
|
||||
elements: str
|
||||
version_added: 2.8
|
||||
vault_credential:
|
||||
description:
|
||||
- Name of the vault credential to use for the job template.
|
||||
- Deprecated, use 'credentials'.
|
||||
version_added: 2.7
|
||||
type: str
|
||||
forks:
|
||||
description:
|
||||
@ -89,7 +93,6 @@ options:
|
||||
description:
|
||||
- Specify C(extra_vars) for the template.
|
||||
type: dict
|
||||
version_added: 3.7
|
||||
job_tags:
|
||||
description:
|
||||
- Comma separated list of the tags to use for the job template.
|
||||
@ -97,7 +100,6 @@ options:
|
||||
force_handlers:
|
||||
description:
|
||||
- Enable forcing playbook handlers to run even if a task fails.
|
||||
version_added: 2.7
|
||||
type: bool
|
||||
default: 'no'
|
||||
aliases:
|
||||
@ -109,12 +111,10 @@ options:
|
||||
start_at_task:
|
||||
description:
|
||||
- Start the playbook at the task matching this name.
|
||||
version_added: 2.7
|
||||
type: str
|
||||
diff_mode:
|
||||
description:
|
||||
- Enable diff mode for the job template.
|
||||
version_added: 2.7
|
||||
type: bool
|
||||
aliases:
|
||||
- diff_mode_enabled
|
||||
@ -122,7 +122,6 @@ options:
|
||||
use_fact_cache:
|
||||
description:
|
||||
- Enable use of fact caching for the job template.
|
||||
version_added: 2.7
|
||||
type: bool
|
||||
default: 'no'
|
||||
aliases:
|
||||
@ -139,7 +138,6 @@ options:
|
||||
ask_diff_mode_on_launch:
|
||||
description:
|
||||
- Prompt user to enable diff mode (show changes) to files when supported by modules.
|
||||
version_added: 2.7
|
||||
type: bool
|
||||
default: 'False'
|
||||
aliases:
|
||||
@ -154,7 +152,6 @@ options:
|
||||
ask_limit_on_launch:
|
||||
description:
|
||||
- Prompt user for a limit on launch.
|
||||
version_added: 2.7
|
||||
type: bool
|
||||
default: 'False'
|
||||
aliases:
|
||||
@ -169,7 +166,6 @@ options:
|
||||
ask_skip_tags_on_launch:
|
||||
description:
|
||||
- Prompt user for job tags to skip on launch.
|
||||
version_added: 2.7
|
||||
type: bool
|
||||
default: 'False'
|
||||
aliases:
|
||||
@ -184,7 +180,6 @@ options:
|
||||
ask_verbosity_on_launch:
|
||||
description:
|
||||
- Prompt user to choose a verbosity level on launch.
|
||||
version_added: 2.7
|
||||
type: bool
|
||||
default: 'False'
|
||||
aliases:
|
||||
@ -206,13 +201,11 @@ options:
|
||||
survey_enabled:
|
||||
description:
|
||||
- Enable a survey on the job template.
|
||||
version_added: 2.7
|
||||
type: bool
|
||||
default: 'no'
|
||||
survey_spec:
|
||||
description:
|
||||
- JSON/YAML dict formatted survey definition.
|
||||
version_added: 2.8
|
||||
type: dict
|
||||
become_enabled:
|
||||
description:
|
||||
@ -222,7 +215,6 @@ options:
|
||||
allow_simultaneous:
|
||||
description:
|
||||
- Allow simultaneous runs of the job template.
|
||||
version_added: 2.7
|
||||
type: bool
|
||||
default: 'no'
|
||||
aliases:
|
||||
@ -232,7 +224,6 @@ options:
|
||||
- Maximum time in seconds to wait for a job to finish (server-side).
|
||||
type: int
|
||||
custom_virtualenv:
|
||||
version_added: "2.9"
|
||||
description:
|
||||
- Local absolute file path containing a custom Python virtualenv to use.
|
||||
type: str
|
||||
@ -299,6 +290,7 @@ EXAMPLES = '''
|
||||
tower_job_template:
|
||||
name: "Ping"
|
||||
job_type: "run"
|
||||
organization: "Default"
|
||||
inventory: "Local"
|
||||
project: "Demo"
|
||||
playbook: "ping.yml"
|
||||
@ -349,6 +341,7 @@ def main():
|
||||
name=dict(required=True),
|
||||
new_name=dict(),
|
||||
description=dict(default=''),
|
||||
organization=dict(),
|
||||
job_type=dict(choices=['run', 'check']),
|
||||
inventory=dict(),
|
||||
project=dict(),
|
||||
@ -415,19 +408,24 @@ def main():
|
||||
credentials = []
|
||||
credentials.append(credential)
|
||||
|
||||
new_fields = {}
|
||||
search_fields = {'name': name}
|
||||
|
||||
# Attempt to look up the related items the user specified (these will fail the module if not found)
|
||||
organization_id = None
|
||||
organization = module.params.get('organization')
|
||||
if organization:
|
||||
organization_id = module.resolve_name_to_id('organizations', organization)
|
||||
search_fields['organization'] = new_fields['organization'] = organization_id
|
||||
|
||||
# Attempt to look up an existing item based on the provided data
|
||||
existing_item = module.get_one('job_templates', **{
|
||||
'data': {
|
||||
'name': name,
|
||||
}
|
||||
})
|
||||
existing_item = module.get_one('job_templates', **{'data': search_fields})
|
||||
|
||||
if state == 'absent':
|
||||
# If the state was absent we can let the module delete it if needed, the module will handle exiting from this
|
||||
module.delete_if_needed(existing_item)
|
||||
|
||||
# Create the data that gets sent for create and update
|
||||
new_fields = {}
|
||||
new_fields['name'] = new_name if new_name else name
|
||||
for field_name in (
|
||||
'description', 'job_type', 'playbook', 'scm_branch', 'forks', 'limit', 'verbosity',
|
||||
@ -454,7 +452,20 @@ def main():
|
||||
if inventory is not None:
|
||||
new_fields['inventory'] = module.resolve_name_to_id('inventories', inventory)
|
||||
if project is not None:
|
||||
new_fields['project'] = module.resolve_name_to_id('projects', project)
|
||||
if organization_id is not None:
|
||||
project_data = module.get_one('projects', **{
|
||||
'data': {
|
||||
'name': project,
|
||||
'organization': organization_id,
|
||||
}
|
||||
})
|
||||
if project_data is None:
|
||||
module.fail_json(msg="The project {0} in organization {1} was not found on the Tower server".format(
|
||||
project, organization
|
||||
))
|
||||
new_fields['project'] = project_data['id']
|
||||
else:
|
||||
new_fields['project'] = module.resolve_name_to_id('projects', project)
|
||||
if webhook_credential is not None:
|
||||
new_fields['webhook_credential'] = module.resolve_name_to_id('credentials', webhook_credential)
|
||||
|
||||
|
||||
@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_job_wait
|
||||
version_added: "2.3"
|
||||
author: "Wayne Witzel III (@wwitzel3)"
|
||||
short_description: Wait for Ansible Tower job to finish.
|
||||
description:
|
||||
@ -141,7 +140,7 @@ def main():
|
||||
interval = abs((min_interval + max_interval) / 2)
|
||||
module.deprecate(
|
||||
msg="Min and max interval have been deprecated, please use interval instead; interval will be set to {0}".format(interval),
|
||||
version="3.7"
|
||||
version="ansible.tower:4.0.0"
|
||||
)
|
||||
|
||||
# Attempt to look up job based on the provided id
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_label
|
||||
author: "Wayne Witzel III (@wwitzel3)"
|
||||
version_added: "2.3"
|
||||
short_description: create, update, or destroy Ansible Tower labels.
|
||||
description:
|
||||
- Create, update, or destroy Ansible Tower labels. See
|
||||
|
||||
@ -16,7 +16,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_license
|
||||
author: "John Westcott IV (@john-westcott-iv)"
|
||||
version_added: "2.9"
|
||||
short_description: Set the license for Ansible Tower
|
||||
description:
|
||||
- Get or Set Ansible Tower license. See
|
||||
@ -27,13 +26,11 @@ options:
|
||||
- The contents of the license file
|
||||
required: True
|
||||
type: dict
|
||||
version_added: "3.7"
|
||||
eula_accepted:
|
||||
description:
|
||||
- Whether or not the EULA is accepted.
|
||||
required: True
|
||||
type: bool
|
||||
version_added: "3.7"
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_notification
|
||||
author: "Samuel Carpentier (@samcarpentier)"
|
||||
version_added: "2.8"
|
||||
short_description: create, update, or destroy Ansible Tower notification.
|
||||
description:
|
||||
- Create, update, or destroy Ansible Tower notifications. See
|
||||
@ -371,7 +370,9 @@ def main():
|
||||
# Deprecation warnings for all other params
|
||||
for legacy_input in OLD_INPUT_NAMES:
|
||||
if module.params.get(legacy_input) is not None:
|
||||
module.deprecate(msg='{0} parameter has been deprecated, please use notification_configuration instead'.format(legacy_input), version="3.6")
|
||||
module.deprecate(
|
||||
msg='{0} parameter has been deprecated, please use notification_configuration instead'.format(legacy_input),
|
||||
version="ansible.tower:4.0.0")
|
||||
|
||||
# Attempt to look up the related items the user specified (these will fail the module if not found)
|
||||
organization_id = None
|
||||
|
||||
@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_organization
|
||||
version_added: "2.3"
|
||||
author: "Wayne Witzel III (@wwitzel3)"
|
||||
short_description: create, update, or destroy Ansible Tower organizations
|
||||
description:
|
||||
@ -33,7 +32,6 @@ options:
|
||||
- The description to use for the organization.
|
||||
type: str
|
||||
custom_virtualenv:
|
||||
version_added: "2.9"
|
||||
description:
|
||||
- Local absolute file path containing a custom Python virtualenv to use.
|
||||
type: str
|
||||
@ -43,7 +41,6 @@ options:
|
||||
- The max hosts allowed in this organizations
|
||||
default: "0"
|
||||
type: int
|
||||
version_added: "3.7"
|
||||
state:
|
||||
description:
|
||||
- Desired state of the resource.
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_project
|
||||
author: "Wayne Witzel III (@wwitzel3)"
|
||||
version_added: "2.3"
|
||||
short_description: create, update, or destroy Ansible Tower projects
|
||||
description:
|
||||
- Create, update, or destroy Ansible Tower projects. See
|
||||
@ -56,7 +55,6 @@ options:
|
||||
- The refspec to use for the SCM resource.
|
||||
type: str
|
||||
default: ''
|
||||
version_added: "3.7"
|
||||
scm_credential:
|
||||
description:
|
||||
- Name of the credential to use with this SCM resource.
|
||||
@ -77,7 +75,6 @@ options:
|
||||
type: bool
|
||||
default: 'no'
|
||||
scm_update_cache_timeout:
|
||||
version_added: "2.8"
|
||||
description:
|
||||
- Cache Timeout to cache prior project syncs for a certain number of seconds.
|
||||
Only valid if scm_update_on_launch is to True, otherwise ignored.
|
||||
@ -87,17 +84,14 @@ options:
|
||||
description:
|
||||
- Allow changing the SCM branch or revision in a job template that uses this project.
|
||||
type: bool
|
||||
version_added: "3.7"
|
||||
aliases:
|
||||
- scm_allow_override
|
||||
job_timeout:
|
||||
version_added: "2.8"
|
||||
description:
|
||||
- The amount of time (in seconds) to run before the SCM Update is canceled. A value of 0 means no timeout.
|
||||
default: 0
|
||||
type: int
|
||||
custom_virtualenv:
|
||||
version_added: "2.8"
|
||||
description:
|
||||
- Local absolute file path containing a custom Python virtualenv to use
|
||||
type: str
|
||||
|
||||
@ -17,11 +17,10 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_receive
|
||||
deprecated:
|
||||
removed_in: "3.7"
|
||||
removed_in: "14.0.0"
|
||||
why: Deprecated in favor of upcoming C(_export) module.
|
||||
alternative: Once published, use M(tower_export) instead.
|
||||
author: "John Westcott IV (@john-westcott-iv)"
|
||||
version_added: "2.8"
|
||||
short_description: Receive assets from Ansible Tower.
|
||||
description:
|
||||
- Receive assets from Ansible Tower. See
|
||||
@ -166,7 +165,7 @@ def main():
|
||||
|
||||
module = TowerModule(argument_spec=argument_spec, supports_check_mode=False)
|
||||
|
||||
module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI export command.", version="3.7")
|
||||
module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI export command.", version="awx.awx:14.0.0")
|
||||
|
||||
if not HAS_TOWER_CLI:
|
||||
module.fail_json(msg='ansible-tower-cli required for this module')
|
||||
|
||||
@ -16,7 +16,6 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_role
|
||||
version_added: "2.3"
|
||||
author: "Wayne Witzel III (@wwitzel3)"
|
||||
short_description: grant or revoke an Ansible Tower role.
|
||||
description:
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_schedule
|
||||
author: "John Westcott IV (@john-westcott-iv)"
|
||||
version_added: "2.3"
|
||||
short_description: create, update, or destroy Ansible Tower schedules.
|
||||
description:
|
||||
- Create, update, or destroy Ansible Tower schedules. See
|
||||
|
||||
@ -17,11 +17,10 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_send
|
||||
deprecated:
|
||||
removed_in: "3.7"
|
||||
removed_in: "14.0.0"
|
||||
why: Deprecated in favor of upcoming C(_import) module.
|
||||
alternative: Once published, use M(tower_import) instead.
|
||||
author: "John Westcott IV (@john-westcott-iv)"
|
||||
version_added: "2.8"
|
||||
short_description: Send assets to Ansible Tower.
|
||||
description:
|
||||
- Send assets to Ansible Tower. See
|
||||
@ -106,7 +105,7 @@ def main():
|
||||
|
||||
module = TowerModule(argument_spec=argument_spec, supports_check_mode=False)
|
||||
|
||||
module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI import command", version="3.7")
|
||||
module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI import command", version="awx.awx:14.0.0")
|
||||
|
||||
if not HAS_TOWER_CLI:
|
||||
module.fail_json(msg='ansible-tower-cli required for this module')
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_settings
|
||||
author: "Nikhil Jain (@jainnikhil30)"
|
||||
version_added: "2.7"
|
||||
short_description: Modify Ansible Tower settings.
|
||||
description:
|
||||
- Modify Ansible Tower settings. See
|
||||
@ -37,7 +36,6 @@ options:
|
||||
description:
|
||||
- A data structure to be sent into the settings endpoint
|
||||
type: dict
|
||||
version_added: "3.7"
|
||||
requirements:
|
||||
- pyyaml
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
@ -82,6 +80,10 @@ except ImportError:
|
||||
|
||||
|
||||
def coerce_type(module, value):
|
||||
# If our value is already None we can just return directly
|
||||
if value is None:
|
||||
return value
|
||||
|
||||
yaml_ish = bool((
|
||||
value.startswith('{') and value.endswith('}')
|
||||
) or (
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_team
|
||||
author: "Wayne Witzel III (@wwitzel3)"
|
||||
version_added: "2.3"
|
||||
short_description: create, update, or destroy Ansible Tower team.
|
||||
description:
|
||||
- Create, update, or destroy Ansible Tower teams. See
|
||||
@ -32,7 +31,6 @@ options:
|
||||
description:
|
||||
- To use when changing a team's name.
|
||||
type: str
|
||||
version_added: "3.7"
|
||||
description:
|
||||
description:
|
||||
- The description to use for the team.
|
||||
|
||||
201
awx_collection/plugins/modules/tower_token.py
Normal file
201
awx_collection/plugins/modules/tower_token.py
Normal 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()
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_user
|
||||
author: "John Westcott IV (@john-westcott-iv)"
|
||||
version_added: "2.3"
|
||||
short_description: create, update, or destroy Ansible Tower users.
|
||||
description:
|
||||
- Create, update, or destroy Ansible Tower users. See
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_workflow_job_template
|
||||
author: "John Westcott IV (@john-westcott-iv)"
|
||||
version_added: "2.3"
|
||||
short_description: create, update, or destroy Ansible Tower workflow job templates.
|
||||
description:
|
||||
- Create, update, or destroy Ansible Tower workflow job templates.
|
||||
|
||||
@ -17,7 +17,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_workflow_job_template_node
|
||||
author: "John Westcott IV (@john-westcott-iv)"
|
||||
version_added: "2.3"
|
||||
short_description: create, update, or destroy Ansible Tower workflow job template nodes.
|
||||
description:
|
||||
- Create, update, or destroy Ansible Tower workflow job template nodes.
|
||||
|
||||
@ -14,7 +14,6 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_workflow_launch
|
||||
author: "John Westcott IV (@john-westcott-iv)"
|
||||
version_added: "2.8"
|
||||
short_description: Run a workflow in Ansible Tower
|
||||
description:
|
||||
- Launch an Ansible Tower workflows. See
|
||||
@ -32,7 +31,6 @@ options:
|
||||
- Organization the workflow job template exists in.
|
||||
- Used to help lookup the object, cannot be modified using this module.
|
||||
- If not provided, will lookup by name only, which does not work with duplicates.
|
||||
required: False
|
||||
type: str
|
||||
inventory:
|
||||
description:
|
||||
@ -47,7 +45,6 @@ options:
|
||||
- A specific branch of the SCM project to run the template on.
|
||||
- This is only applicable if your project allows for branch override.
|
||||
type: str
|
||||
version_added: "3.7"
|
||||
extra_vars:
|
||||
description:
|
||||
- Any extra vars required to launch the job.
|
||||
|
||||
@ -17,11 +17,10 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_workflow_template
|
||||
deprecated:
|
||||
removed_in: "3.7"
|
||||
removed_in: "14.0.0"
|
||||
why: Deprecated in favor of C(_workflow_job_template) and C(_workflow_job_template_node) modules.
|
||||
alternative: Use M(tower_workflow_job_template) and M(_workflow_job_template_node) instead.
|
||||
author: "Adrien Fleury (@fleu42)"
|
||||
version_added: "2.7"
|
||||
short_description: create, update, or destroy Ansible Tower workflow template.
|
||||
description:
|
||||
- A tower-cli based module for CRUD actions on workflow job templates.
|
||||
@ -37,12 +36,10 @@ options:
|
||||
description:
|
||||
- Prompt user for (extra_vars) on launch.
|
||||
type: bool
|
||||
version_added: "2.9"
|
||||
ask_inventory:
|
||||
description:
|
||||
- Prompt user for inventory on launch.
|
||||
type: bool
|
||||
version_added: "2.9"
|
||||
description:
|
||||
description:
|
||||
- The description to use for the workflow.
|
||||
@ -54,7 +51,6 @@ options:
|
||||
inventory:
|
||||
description:
|
||||
- Name of the inventory to use for the job template.
|
||||
version_added: "2.9"
|
||||
type: str
|
||||
name:
|
||||
description:
|
||||
@ -153,7 +149,7 @@ def main():
|
||||
"This module is replaced by the combination of tower_workflow_job_template and "
|
||||
"tower_workflow_job_template_node. This uses the old tower-cli and wll be "
|
||||
"removed in 2022."
|
||||
), version='4.2.0')
|
||||
), version='awx.awx:14.0.0')
|
||||
|
||||
name = module.params.get('name')
|
||||
state = module.params.get('state')
|
||||
|
||||
@ -71,21 +71,14 @@ def test_duplicate_config(collection_import, silence_warning):
|
||||
'tower_config_file': 'my_config'
|
||||
}
|
||||
|
||||
class DuplicateTestTowerModule(TowerModule):
|
||||
def load_config(self, config_path):
|
||||
assert config_path == 'my_config'
|
||||
|
||||
def _load_params(self):
|
||||
self.params = data
|
||||
|
||||
cli_data = {'ANSIBLE_MODULE_ARGS': data}
|
||||
testargs = ['module_file.py', json.dumps(cli_data)]
|
||||
with mock.patch.object(sys, 'argv', testargs):
|
||||
with mock.patch.object(TowerModule, 'load_config') as mock_load:
|
||||
argument_spec = dict(
|
||||
name=dict(required=True),
|
||||
zig=dict(type='str'),
|
||||
)
|
||||
DuplicateTestTowerModule(argument_spec=argument_spec)
|
||||
TowerModule(argument_spec=argument_spec, direct_params=data)
|
||||
assert mock_load.mock_calls[-1] == mock.call('my_config')
|
||||
|
||||
silence_warning.assert_called_once_with(
|
||||
'The parameter(s) tower_username were provided at the same time as '
|
||||
'tower_config_file. Precedence may be unstable, '
|
||||
|
||||
29
awx_collection/test/awx/test_token.py
Normal file
29
awx_collection/test/awx/test_token.py
Normal 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'
|
||||
@ -74,3 +74,14 @@
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
|
||||
- name: Handle an omit value
|
||||
tower_settings:
|
||||
name: AWX_PROOT_BASE_PATH
|
||||
value: '{{ junk_var | default(omit) }}'
|
||||
register: result
|
||||
ignore_errors: true
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "'Unable to update settings' in result.msg"
|
||||
|
||||
@ -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
|
||||
@ -1,6 +1,6 @@
|
||||
plugins/modules/tower_receive.py validate-modules:deprecation-mismatch
|
||||
plugins/modules/tower_receive.py validate-modules:invalid-documentation
|
||||
plugins/modules/tower_send.py validate-modules:deprecation-mismatch
|
||||
plugins/modules/tower_send.py validate-modules:invalid-documentation
|
||||
plugins/modules/tower_workflow_template.py validate-modules:deprecation-mismatch
|
||||
plugins/modules/tower_workflow_template.py validate-modules:invalid-documentation
|
||||
plugins/modules/tower_credential.py pylint:wrong-collection-deprecated-version-tag
|
||||
plugins/modules/tower_job_wait.py pylint:wrong-collection-deprecated-version-tag
|
||||
plugins/modules/tower_notification.py pylint:wrong-collection-deprecated-version-tag
|
||||
|
||||
@ -24,7 +24,7 @@ DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_{{ singular_item_type }}
|
||||
author: "John Westcott IV (@john-westcott-iv)"
|
||||
version_added: "2.3"
|
||||
version_added: "4.0"
|
||||
short_description: create, update, or destroy Ansible Tower {{ human_readable }}.
|
||||
description:
|
||||
- Create, update, or destroy Ansible Tower {{ human_readable }}. See
|
||||
@ -87,7 +87,6 @@ options:
|
||||
- The Tower OAuth token to use.
|
||||
required: False
|
||||
type: str
|
||||
version_added: "3.7"
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
|
||||
@ -30,6 +30,13 @@
|
||||
path: "{{ collection_path }}/plugins/inventory/tower.py"
|
||||
regexp: "^ NAME = 'awx.awx.tower' # REPLACE$"
|
||||
replace: " NAME = '{{ collection_namespace }}.{{ collection_package }}.tower' # REPLACE"
|
||||
|
||||
- name: Get sanity tests to work with non-default name
|
||||
lineinfile:
|
||||
path: "{{ collection_path }}/tests/sanity/ignore-2.10.txt"
|
||||
state: absent
|
||||
regexp: ' pylint:wrong-collection-deprecated-version-tag$'
|
||||
|
||||
when:
|
||||
- (collection_package != 'awx') or (collection_namespace != 'awx')
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -220,7 +220,6 @@ RUN for dir in \
|
||||
/vendor ; \
|
||||
do mkdir -m 0775 -p $dir ; chmod g+rw $dir ; chgrp root $dir ; done && \
|
||||
for file in \
|
||||
/etc/supervisord.conf \
|
||||
/var/run/nginx.pid \
|
||||
/venv/awx/lib/python3.6/site-packages/awx.egg-link ; \
|
||||
do touch $file ; chmod g+rw $file ; done
|
||||
|
||||
@ -12,6 +12,8 @@ ANSIBLE_REMOTE_TEMP=/tmp ANSIBLE_LOCAL_TEMP=/tmp ansible -i "127.0.0.1," -c loca
|
||||
|
||||
if [ -z "$AWX_SKIP_MIGRATIONS" ]; then
|
||||
awx-manage migrate --noinput
|
||||
awx-manage provision_instance --hostname=$(hostname)
|
||||
awx-manage register_queue --queuename=tower --instance_percent=100
|
||||
fi
|
||||
|
||||
if [ ! -z "$AWX_ADMIN_USER" ]&&[ ! -z "$AWX_ADMIN_PASSWORD" ]; then
|
||||
@ -21,8 +23,6 @@ if [ ! -z "$AWX_ADMIN_USER" ]&&[ ! -z "$AWX_ADMIN_PASSWORD" ]; then
|
||||
{% endif %}
|
||||
fi
|
||||
echo 'from django.conf import settings; x = settings.AWX_TASK_ENV; x["HOME"] = "/var/lib/awx"; settings.AWX_TASK_ENV = x' | awx-manage shell
|
||||
awx-manage provision_instance --hostname=$(hostname)
|
||||
awx-manage register_queue --queuename=tower --instance_percent=100
|
||||
|
||||
unset $(cut -d = -f -1 /etc/tower/conf.d/environment.sh)
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ collections:
|
||||
- name: amazon.aws
|
||||
version: 0.1.1 # version 0.1.0 seems to have gone missing
|
||||
- name: theforeman.foreman
|
||||
version: 0.8.0
|
||||
version: 0.8.1
|
||||
- name: google.cloud
|
||||
version: 0.0.9 # contains PR 167, should be good to go
|
||||
- name: openstack.cloud
|
||||
|
||||
@ -31,6 +31,7 @@ services:
|
||||
- "../:/awx_devel"
|
||||
- "./redis/redis_socket_ha_1:/var/run/redis/"
|
||||
- "./memcached/:/var/run/memcached"
|
||||
- "./docker-compose/supervisor.conf:/etc/supervisord.conf"
|
||||
ports:
|
||||
- "5899-5999:5899-5999"
|
||||
awx-2:
|
||||
@ -50,6 +51,7 @@ services:
|
||||
- "../:/awx_devel"
|
||||
- "./redis/redis_socket_ha_2:/var/run/redis/"
|
||||
- "./memcached/:/var/run/memcached"
|
||||
- "./docker-compose/supervisor.conf:/etc/supervisord.conf"
|
||||
ports:
|
||||
- "7899-7999:7899-7999"
|
||||
awx-3:
|
||||
@ -69,6 +71,7 @@ services:
|
||||
- "../:/awx_devel"
|
||||
- "./redis/redis_socket_ha_3:/var/run/redis/"
|
||||
- "./memcached/:/var/run/memcached"
|
||||
- "./docker-compose/supervisor.conf:/etc/supervisord.conf"
|
||||
ports:
|
||||
- "8899-8999:8899-8999"
|
||||
redis_1:
|
||||
|
||||
@ -34,7 +34,7 @@ services:
|
||||
- "../awx/projects/:/var/lib/awx/projects/"
|
||||
- "./redis/redis_socket_standalone:/var/run/redis/"
|
||||
- "./memcached/:/var/run/memcached"
|
||||
- "./rsyslog/:/var/lib/awx/rsyslog"
|
||||
- "./docker-compose/supervisor.conf:/etc/supervisord.conf"
|
||||
privileged: true
|
||||
tty: true
|
||||
# A useful container that simply passes through log messages to the console
|
||||
|
||||
@ -20,7 +20,6 @@ else
|
||||
fi
|
||||
|
||||
make awx-link
|
||||
yes | cp -rf /awx_devel/tools/docker-compose/supervisor.conf /etc/supervisord.conf
|
||||
|
||||
# AWX bootstrapping
|
||||
make version_file
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user