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