Merge pull request #1 from ansible/devel

Merging remote to devel
This commit is contained in:
Sean Sullivan 2020-06-09 07:50:36 -05:00 committed by GitHub
commit 104073af45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
90 changed files with 931 additions and 361 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": {

View File

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

View File

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

View File

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

View File

@ -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);
});
});

View File

@ -1,11 +1,29 @@
import React from 'react';
import React, { Fragment } from 'react';
import styled from 'styled-components';
import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons';
import { Tooltip } from '@patternfly/react-core';
import { t } from '@lingui/macro';
import { useFormikContext } from 'formik';
import { 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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {

View File

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

View File

@ -0,0 +1,26 @@
import React from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import ApplicationEdit from '../ApplicationEdit';
import ApplicationDetails from '../ApplicationDetails';
function Application() {
return (
<>
<Switch>
<Redirect
from="/applications/:id"
to="/applications/:id/details"
exact
/>
<Route path="/applications/:id/edit">
<ApplicationEdit />
</Route>
<Route path="/applications/:id/details">
<ApplicationDetails />
</Route>
</Switch>
</>
);
}
export default Application;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,49 @@
import React, { Component, Fragment } from 'react';
import React, { useState, useCallback } from 'react';
import { withI18n } from '@lingui/react';
import { 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);

View File

@ -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');
});
});

View File

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

View File

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

View File

@ -32,6 +32,11 @@ const DataListAction = styled(_DataListAction)`
grid-gap: 16px;
grid-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>,
]}
/>

View File

@ -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);
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,201 @@
#!/usr/bin/python
# coding: utf-8 -*-
# (c) 2020, John Westcott IV <john.westcott.iv@redhat.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: tower_token
author: "John Westcott IV (@john-westcott-iv)"
version_added: "2.3"
short_description: create, update, or destroy Ansible Tower tokens.
description:
- Create or destroy Ansible Tower tokens. See
U(https://www.ansible.com/tower) for an overview.
- In addition, the module sets an Ansible fact which can be passed into other
tower_* modules as the parameter tower_oauthtoken. See examples for usage.
- Because of the sensitive nature of tokens, the created token value is only available once
through the Ansible fact. (See RETURN for details)
- Due to the nature of tokens in Tower this module is not idempotent. A second will
with the same parameters will create a new token.
- If you are creating a temporary token for use with modules you should delete the token
when you are done with it. See the example for how to do it.
options:
description:
description:
- Optional description of this access token.
required: False
type: str
default: ''
application:
description:
- The application tied to this token.
required: False
type: str
scope:
description:
- Allowed scopes, further restricts user's permissions. Must be a simple space-separated string with allowed scopes ['read', 'write'].
required: False
type: str
default: 'write'
choices: ["read", "write"]
existing_token:
description: The data structure produced from tower_token in create mode to be used with state absent.
type: dict
existing_token_id:
description: A token ID (number) which can be used to delete an arbitrary token with state absent.
type: str
state:
description:
- Desired state of the resource.
choices: ["present", "absent"]
default: "present"
type: str
extends_documentation_fragment: awx.awx.auth
'''
EXAMPLES = '''
- block:
- name: Create a new token using an existing token
tower_token:
description: '{{ token_description }}'
scope: "write"
state: present
tower_oauthtoken: "{{ my_existing_token }}"
- name: Delete this token
tower_token:
existing_token: "{{ tower_token }}"
state: absent
- name: Create a new token using username/password
tower_token:
description: '{{ token_description }}'
scope: "write"
state: present
tower_username: "{{ my_username }}"
tower_password: "{{ my_password }}"
- name: Use our new token to make another call
tower_job_list:
tower_oauthtoken: "{{ tower_token }}"
always:
- name: Delete our Token with the token we created
tower_token:
existing_token: "{{ tower_token }}"
state: absent
when: tower_token is defined
- name: Delete a token by its id
tower_token:
existing_token_id: 4
state: absent
'''
RETURN = '''
tower_token:
type: dict
description: An Ansible Fact variable representing a Tower token object which can be used for auth in subsequent modules. See examples for usage.
contains:
token:
description: The token that was generated. This token can never be accessed again, make sure this value is noted before it is lost.
type: str
id:
description: The numeric ID of the token created
type: str
returned: on successful create
'''
from ..module_utils.tower_api import TowerModule
def return_token(module, last_response):
# A token is special because you can never get the actual token ID back from the API.
# So the default module return would give you an ID but then the token would forever be masked on you.
# This method will return the entire token object we got back so that a user has access to the token
module.json_output['ansible_facts'] = {
'tower_token': last_response,
}
module.exit_json(**module.json_output)
def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict(
description=dict(),
application=dict(),
scope=dict(choices=['read', 'write'], default='write'),
existing_token=dict(type='dict'),
existing_token_id=dict(),
state=dict(choices=['present', 'absent'], default='present'),
)
# Create a module for ourselves
module = TowerModule(
argument_spec=argument_spec,
mutually_exclusive=[
('existing_token', 'existing_token_id'),
],
# If we are state absent make sure one of existing_token or existing_token_id are present
required_if=[
['state', 'absent', ('existing_token', 'existing_token_id'), True, ],
],
)
# Extract our parameters
description = module.params.get('description')
application = module.params.get('application')
scope = module.params.get('scope')
existing_token = module.params.get('existing_token')
existing_token_id = module.params.get('existing_token_id')
state = module.params.get('state')
if state == 'absent':
if not existing_token:
existing_token = module.get_one('tokens', **{
'data': {
'id': existing_token_id,
}
})
# If the state was absent we can let the module delete it if needed, the module will handle exiting from this
module.delete_if_needed(existing_token)
# Attempt to look up the related items the user specified (these will fail the module if not found)
application_id = None
if application:
application_id = module.resolve_name_to_id('applications', application)
# Create the data that gets sent for create and update
new_fields = {}
if description is not None:
new_fields['description'] = description
if application is not None:
new_fields['application'] = application_id
if scope is not None:
new_fields['scope'] = scope
# If the state was present and we can let the module build or update the existing item, this will return on its own
module.create_or_update_if_needed(
None, new_fields,
endpoint='tokens', item_type='token',
associations={
},
on_create=return_token,
)
if __name__ == '__main__':
main()

View File

@ -17,7 +17,6 @@ DOCUMENTATION = '''
---
module: tower_user
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,29 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import pytest
from awx.main.models import OAuth2AccessToken
@pytest.mark.django_db
def test_create_token(run_module, admin_user):
module_args = {
'description': 'barfoo',
'state': 'present',
'scope': 'read',
'tower_host': None,
'tower_username': None,
'tower_password': None,
'validate_certs': None,
'tower_oauthtoken': None,
'tower_config_file': None,
}
result = run_module('tower_token', module_args, admin_user)
assert result.get('changed'), result
tokens = OAuth2AccessToken.objects.filter(description='barfoo')
assert len(tokens) == 1, 'Tokens with description of barfoo != 0: {0}'.format(len(tokens))
assert tokens[0].scope == 'read', 'Token was not given read access'

View File

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

View File

@ -0,0 +1,110 @@
---
- name: Generate names
set_fact:
token_description: "AWX-Collection-tests-tower_token-description-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
- name: Try to use a token as a dict which is missing the token parameter
tower_job_list:
tower_oauthtoken:
not_token: "This has no token entry"
register: results
ignore_errors: true
- assert:
that:
- results is failed
- '"The provided dict in tower_oauthtoken did not properly contain the token entry" == results.msg'
- name: Try to use a token as a list
tower_job_list:
tower_oauthtoken:
- dummy_token
register: results
ignore_errors: true
- assert:
that:
- results is failed
- '"The provided tower_oauthtoken type was not valid (list). Valid options are str or dict." == results.msg'
- name: Try to delete a token with no existing_token or existing_token_id
tower_token:
state: absent
register: results
ignore_errors: true
- assert:
that:
- results is failed
# We don't assert a message here because it handled by ansible
- name: Try to delete a token with both existing_token or existing_token_id
tower_token:
existing_token:
id: 1234
existing_token_id: 1234
state: absent
register: results
ignore_errors: true
- assert:
that:
- results is failed
# We don't assert a message here because it handled by ansible
- block:
- name: Create a Token
tower_token:
description: '{{ token_description }}'
scope: "write"
state: present
register: new_token
- name: Validate our token works by token
tower_job_list:
tower_oauthtoken: "{{ tower_token.token }}"
register: job_list
- name: Validate out token works by object
tower_job_list:
tower_oauthtoken: "{{ tower_token }}"
register: job_list
always:
- name: Delete our Token with our own token
tower_token:
existing_token: "{{ tower_token }}"
tower_oauthtoken: "{{ tower_token }}"
state: absent
when: tower_token is defined
register: results
- assert:
that:
- results is changed or results is skipped
- block:
- name: Create a second token
tower_token:
description: '{{ token_description }}'
scope: "write"
state: present
register: results
- assert:
that:
- results is changed
always:
- name: Delete the second Token with our own token
tower_token:
existing_token_id: "{{ tower_token['id'] }}"
tower_oauthtoken: "{{ tower_token }}"
state: absent
when: tower_token is defined
register: results
- assert:
that:
- results is changed or resuslts is skipped

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,8 @@ ANSIBLE_REMOTE_TEMP=/tmp ANSIBLE_LOCAL_TEMP=/tmp ansible -i "127.0.0.1," -c loca
if [ -z "$AWX_SKIP_MIGRATIONS" ]; then
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)

View File

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

View File

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

View File

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

View File

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