mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Merge branch 'release_2.4.0' into devel
* release_2.4.0: (70 commits) Disallow changing a users password for social auth bump django-radius to include new license Change Jenkins server references from IP to DNS name Social auth and SSO updates: allow multi-org expired licenses to delete orgs Add social log messages to tower's log on error Update default scopes requested for github social auth. Switch to matburt's fork for python social auth Emit a warning for unmapped SAML paramters Fix up some SAML issues Remove `environment` play stmt Use correct aw_repo_url when building packer Add missing libxmlsec1 openssl dependency for ubuntu adds socket tests added git pre commit hook to run flake8 fixes remove_instance error null pointer exception Resolve issue on precise keeping old debs around Attach handlers to django_auth_ldap Attach handlers to django_auth_ldap ...
This commit is contained in:
commit
3b07773bcd
@ -15,6 +15,7 @@ recursive-exclude awx/main/tests *
|
||||
recursive-exclude awx/ui/client *
|
||||
recursive-exclude awx/settings local_settings.py*
|
||||
include tools/scripts/request_tower_configuration.sh
|
||||
include tools/scripts/request_tower_configuration.ps1
|
||||
include tools/scripts/ansible-tower-service
|
||||
include tools/munin_monitors/*
|
||||
include tools/sosreport/*
|
||||
|
||||
9
Makefile
9
Makefile
@ -2,6 +2,7 @@ PYTHON = python
|
||||
SITELIB=$(shell $(PYTHON) -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")
|
||||
OFFICIAL ?= no
|
||||
PACKER ?= packer
|
||||
PACKER_BUILD_OPTS ?= -var 'official=$(OFFICIAL)' -var 'aw_repo_url=$(AW_REPO_URL)'
|
||||
GRUNT ?= $(shell [ -t 0 ] && echo "grunt" || echo "grunt --no-color")
|
||||
TESTEM ?= ./node_modules/.bin/testem
|
||||
TESTEM_DEBUG_BROWSER ?= Chrome
|
||||
@ -10,7 +11,7 @@ MOCHA_BIN ?= ./node_modules/.bin/mocha
|
||||
NODE ?= node
|
||||
NPM_BIN ?= npm
|
||||
DEPS_SCRIPT ?= packaging/bundle/deps.py
|
||||
AW_REPO_URL ?= "http://releases.ansible.com/ansible-tower"
|
||||
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
CLIENT_TEST_DIR ?= build_test
|
||||
|
||||
@ -33,8 +34,10 @@ GIT_REMOTE_URL = $(shell git config --get remote.origin.url)
|
||||
BUILD = 0.git$(DATE)
|
||||
ifeq ($(OFFICIAL),yes)
|
||||
RELEASE ?= 1
|
||||
AW_REPO_URL ?= http://releases.ansible.com/ansible-tower
|
||||
else
|
||||
RELEASE ?= $(BUILD)
|
||||
AW_REPO_URL ?= http://jenkins.testing.ansible.com/ansible-tower_nightlies_RTYUIOPOIUYTYU/$(GIT_BRANCH)
|
||||
endif
|
||||
|
||||
# Allow AMI license customization
|
||||
@ -57,11 +60,9 @@ endif
|
||||
ifeq ($(OFFICIAL),yes)
|
||||
SETUP_TAR_NAME=$(NAME)-setup-$(VERSION)
|
||||
SDIST_TAR_NAME=$(NAME)-$(VERSION)
|
||||
PACKER_BUILD_OPTS ?= -var-file=vars-release.json
|
||||
else
|
||||
SETUP_TAR_NAME=$(NAME)-setup-$(VERSION)-$(RELEASE)
|
||||
SDIST_TAR_NAME=$(NAME)-$(VERSION)-$(RELEASE)
|
||||
PACKER_BUILD_OPTS ?= -var-file=vars-nightly.json
|
||||
endif
|
||||
SDIST_TAR_FILE=$(SDIST_TAR_NAME).tar.gz
|
||||
SETUP_TAR_FILE=$(SETUP_TAR_NAME).tar.gz
|
||||
@ -650,7 +651,7 @@ reprepro: deb-build/$(DEB_NVRA).deb reprepro/conf
|
||||
$(REPREPRO_BIN) $(REPREPRO_OPTS) clearvanished
|
||||
for COMPONENT in non-free $(VERSION); do \
|
||||
$(REPREPRO_BIN) $(REPREPRO_OPTS) -C $$COMPONENT remove $(DEB_DIST) $(NAME) ; \
|
||||
$(REPREPRO_BIN) $(REPREPRO_OPTS) -C $$COMPONENT --keepunreferencedfiles --ignore=brokenold includedeb $(DEB_DIST) deb-build/$(DEB_NVRA).deb ; \
|
||||
$(REPREPRO_BIN) $(REPREPRO_OPTS) -C $$COMPONENT --ignore=brokenold includedeb $(DEB_DIST) deb-build/$(DEB_NVRA).deb ; \
|
||||
done
|
||||
|
||||
|
||||
|
||||
@ -630,6 +630,12 @@ class UserSerializer(BaseSerializer):
|
||||
new_password = None
|
||||
except AttributeError:
|
||||
pass
|
||||
if (getattr(settings, 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', None) or
|
||||
getattr(settings, 'SOCIAL_AUTH_GITHUB_KEY', None) or
|
||||
getattr(settings, 'SOCIAL_AUTH_GITHUB_ORG_KEY', None) or
|
||||
getattr(settings, 'SOCIAL_AUTH_GITHUB_TEAM_KEY', None) or
|
||||
getattr(settings, 'SOCIAL_AUTH_SAML_ENABLED_IDPS', None)) and obj.social_auth.all():
|
||||
new_password = None
|
||||
if new_password:
|
||||
obj.set_password(new_password)
|
||||
if not obj.password:
|
||||
@ -2004,11 +2010,12 @@ class ScheduleSerializer(BaseSerializer):
|
||||
class ActivityStreamSerializer(BaseSerializer):
|
||||
|
||||
changes = serializers.SerializerMethodField('get_changes')
|
||||
object_association = serializers.SerializerMethodField('get_object_association')
|
||||
|
||||
class Meta:
|
||||
model = ActivityStream
|
||||
fields = ('*', '-name', '-description', '-created', '-modified',
|
||||
'timestamp', 'operation', 'changes', 'object1', 'object2')
|
||||
'timestamp', 'operation', 'changes', 'object1', 'object2', 'object_association')
|
||||
|
||||
def get_fields(self):
|
||||
ret = super(ActivityStreamSerializer, self).get_fields()
|
||||
@ -2033,6 +2040,13 @@ class ActivityStreamSerializer(BaseSerializer):
|
||||
logger.warn("Error deserializing activity stream json changes")
|
||||
return {}
|
||||
|
||||
def get_object_association(self, obj):
|
||||
try:
|
||||
return obj.object_relationship_type.split(".")[-1].split("_")[1]
|
||||
except:
|
||||
pass
|
||||
return ""
|
||||
|
||||
def get_related(self, obj):
|
||||
rel = {}
|
||||
if obj.actor is not None:
|
||||
|
||||
@ -11,6 +11,7 @@ import time
|
||||
import socket
|
||||
import sys
|
||||
import errno
|
||||
from base64 import b64encode
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
@ -526,7 +527,10 @@ class AuthView(APIView):
|
||||
def get(self, request):
|
||||
data = SortedDict()
|
||||
err_backend, err_message = request.session.get('social_auth_error', (None, None))
|
||||
for name, backend in load_backends(settings.AUTHENTICATION_BACKENDS).items():
|
||||
auth_backends = load_backends(settings.AUTHENTICATION_BACKENDS).items()
|
||||
# Return auth backends in consistent order: Google, GitHub, SAML.
|
||||
auth_backends.sort(key=lambda x: 'g' if x[0] == 'google-oauth2' else x[0])
|
||||
for name, backend in auth_backends:
|
||||
if (not feature_exists('enterprise_auth') and
|
||||
not feature_enabled('ldap')) or \
|
||||
(not feature_enabled('enterprise_auth') and
|
||||
@ -540,7 +544,7 @@ class AuthView(APIView):
|
||||
}
|
||||
if name == 'saml':
|
||||
backend_data['metadata_url'] = reverse('sso:saml_metadata')
|
||||
for idp in settings.SOCIAL_AUTH_SAML_ENABLED_IDPS.keys():
|
||||
for idp in sorted(settings.SOCIAL_AUTH_SAML_ENABLED_IDPS.keys()):
|
||||
saml_backend_data = dict(backend_data.items())
|
||||
saml_backend_data['login_url'] = '%s?idp=%s' % (login_url, idp)
|
||||
full_backend_name = '%s:%s' % (name, idp)
|
||||
@ -2861,8 +2865,10 @@ class UnifiedJobStdout(RetrieveAPIView):
|
||||
return Response({'range': {'start': 0, 'end': 1, 'absolute_end': 1}, 'content': response_message})
|
||||
else:
|
||||
return Response(response_message)
|
||||
|
||||
|
||||
if request.accepted_renderer.format in ('html', 'api', 'json'):
|
||||
content_format = request.QUERY_PARAMS.get('content_format', 'html')
|
||||
content_encoding = request.QUERY_PARAMS.get('content_encoding', None)
|
||||
start_line = request.QUERY_PARAMS.get('start_line', 0)
|
||||
end_line = request.QUERY_PARAMS.get('end_line', None)
|
||||
dark_val = request.QUERY_PARAMS.get('dark', '')
|
||||
@ -2883,7 +2889,10 @@ class UnifiedJobStdout(RetrieveAPIView):
|
||||
if request.accepted_renderer.format == 'api':
|
||||
return Response(mark_safe(data))
|
||||
if request.accepted_renderer.format == 'json':
|
||||
return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': body})
|
||||
if content_encoding == 'base64' and content_format == 'ansi':
|
||||
return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': b64encode(content)})
|
||||
elif content_format == 'html':
|
||||
return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': body})
|
||||
return Response(data)
|
||||
elif request.accepted_renderer.format == 'ansi':
|
||||
return Response(unified_job.result_stdout_raw)
|
||||
|
||||
@ -146,7 +146,7 @@ class BaseAccess(object):
|
||||
def can_unattach(self, obj, sub_obj, relationship):
|
||||
return self.can_change(obj, None)
|
||||
|
||||
def check_license(self, add_host=False, feature=None):
|
||||
def check_license(self, add_host=False, feature=None, check_expiration=True):
|
||||
reader = TaskSerializer()
|
||||
validation_info = reader.from_file()
|
||||
if ('test' in sys.argv or 'jenkins' in sys.argv) and not os.environ.get('SKIP_LICENSE_FIXUP_FOR_TEST', ''):
|
||||
@ -154,9 +154,9 @@ class BaseAccess(object):
|
||||
validation_info['time_remaining'] = 99999999
|
||||
validation_info['grace_period_remaining'] = 99999999
|
||||
|
||||
if validation_info.get('time_remaining', None) is None:
|
||||
if check_expiration and validation_info.get('time_remaining', None) is None:
|
||||
raise PermissionDenied("license is missing")
|
||||
if validation_info.get("grace_period_remaining") <= 0:
|
||||
if check_expiration and validation_info.get("grace_period_remaining") <= 0:
|
||||
raise PermissionDenied("license has expired")
|
||||
|
||||
free_instances = validation_info.get('free_instances', 0)
|
||||
@ -262,7 +262,7 @@ class OrganizationAccess(BaseAccess):
|
||||
self.user in obj.admins.all())
|
||||
|
||||
def can_delete(self, obj):
|
||||
self.check_license(feature='multiple_organizations')
|
||||
self.check_license(feature='multiple_organizations', check_expiration=False)
|
||||
return self.can_change(obj, None)
|
||||
|
||||
class InventoryAccess(BaseAccess):
|
||||
|
||||
@ -1,128 +0,0 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
# Django
|
||||
from django.dispatch import receiver
|
||||
|
||||
# django-auth-ldap
|
||||
from django_auth_ldap.backend import LDAPSettings as BaseLDAPSettings
|
||||
from django_auth_ldap.backend import LDAPBackend as BaseLDAPBackend
|
||||
from django_auth_ldap.backend import populate_user
|
||||
|
||||
# Ansible Tower
|
||||
from awx.api.license import feature_enabled
|
||||
|
||||
class LDAPSettings(BaseLDAPSettings):
|
||||
|
||||
defaults = dict(BaseLDAPSettings.defaults.items() + {
|
||||
'ORGANIZATION_MAP': {},
|
||||
'TEAM_MAP': {},
|
||||
}.items())
|
||||
|
||||
class LDAPBackend(BaseLDAPBackend):
|
||||
'''
|
||||
Custom LDAP backend for AWX.
|
||||
'''
|
||||
|
||||
settings_prefix = 'AUTH_LDAP_'
|
||||
|
||||
def _get_settings(self):
|
||||
if self._settings is None:
|
||||
self._settings = LDAPSettings(self.settings_prefix)
|
||||
return self._settings
|
||||
|
||||
def _set_settings(self, settings):
|
||||
self._settings = settings
|
||||
|
||||
settings = property(_get_settings, _set_settings)
|
||||
|
||||
def authenticate(self, username, password):
|
||||
if not self.settings.SERVER_URI or not feature_enabled('ldap'):
|
||||
return None
|
||||
return super(LDAPBackend, self).authenticate(username, password)
|
||||
|
||||
def get_user(self, user_id):
|
||||
if not self.settings.SERVER_URI or not feature_enabled('ldap'):
|
||||
return None
|
||||
return super(LDAPBackend, self).get_user(user_id)
|
||||
|
||||
# Disable any LDAP based authorization / permissions checking.
|
||||
|
||||
def has_perm(self, user, perm, obj=None):
|
||||
return False
|
||||
|
||||
def has_module_perms(self, user, app_label):
|
||||
return False
|
||||
|
||||
def get_all_permissions(self, user, obj=None):
|
||||
return set()
|
||||
|
||||
def get_group_permissions(self, user, obj=None):
|
||||
return set()
|
||||
|
||||
def _update_m2m_from_groups(user, ldap_user, rel, opts, remove=False):
|
||||
'''
|
||||
Hepler function to update m2m relationship based on LDAP group membership.
|
||||
'''
|
||||
should_add = False
|
||||
if opts is None:
|
||||
return
|
||||
elif not opts:
|
||||
pass
|
||||
elif opts is True:
|
||||
should_add = True
|
||||
else:
|
||||
if isinstance(opts, basestring):
|
||||
opts = [opts]
|
||||
for group_dn in opts:
|
||||
if not isinstance(group_dn, basestring):
|
||||
continue
|
||||
if ldap_user._get_groups().is_member_of(group_dn):
|
||||
should_add = True
|
||||
if should_add:
|
||||
rel.add(user)
|
||||
elif remove:
|
||||
rel.remove(user)
|
||||
|
||||
@receiver(populate_user)
|
||||
def on_populate_user(sender, **kwargs):
|
||||
'''
|
||||
Handle signal from LDAP backend to populate the user object. Update user
|
||||
organization/team memberships according to their LDAP groups.
|
||||
'''
|
||||
from awx.main.models import Organization, Team
|
||||
user = kwargs['user']
|
||||
ldap_user = kwargs['ldap_user']
|
||||
backend = ldap_user.backend
|
||||
|
||||
# Update organization membership based on group memberships.
|
||||
org_map = getattr(backend.settings, 'ORGANIZATION_MAP', {})
|
||||
for org_name, org_opts in org_map.items():
|
||||
org, created = Organization.objects.get_or_create(name=org_name)
|
||||
remove = bool(org_opts.get('remove', False))
|
||||
admins_opts = org_opts.get('admins', None)
|
||||
remove_admins = bool(org_opts.get('remove_admins', remove))
|
||||
_update_m2m_from_groups(user, ldap_user, org.admins, admins_opts,
|
||||
remove_admins)
|
||||
users_opts = org_opts.get('users', None)
|
||||
remove_users = bool(org_opts.get('remove_users', remove))
|
||||
_update_m2m_from_groups(user, ldap_user, org.users, users_opts,
|
||||
remove_users)
|
||||
|
||||
# Update team membership based on group memberships.
|
||||
team_map = getattr(backend.settings, 'TEAM_MAP', {})
|
||||
for team_name, team_opts in team_map.items():
|
||||
if 'organization' not in team_opts:
|
||||
continue
|
||||
org, created = Organization.objects.get_or_create(name=team_opts['organization'])
|
||||
team, created = Team.objects.get_or_create(name=team_name, organization=org)
|
||||
users_opts = team_opts.get('users', None)
|
||||
remove = bool(team_opts.get('remove', False))
|
||||
_update_m2m_from_groups(user, ldap_user, team.users, users_opts,
|
||||
remove)
|
||||
|
||||
# Update user profile to store LDAP DN.
|
||||
profile = user.profile
|
||||
if profile.ldap_dn != ldap_user.dn:
|
||||
profile.ldap_dn = ldap_user.dn
|
||||
profile.save()
|
||||
@ -21,6 +21,7 @@ class BaseCommandInstance(BaseCommand):
|
||||
|
||||
def __init__(self):
|
||||
super(BaseCommandInstance, self).__init__()
|
||||
self.enforce_primary_role = False
|
||||
self.enforce_roles = False
|
||||
self.enforce_hostname_set = False
|
||||
self.enforce_unique_find = False
|
||||
|
||||
@ -46,43 +46,52 @@ class SocketSession(object):
|
||||
return bool(not auth_token.is_expired())
|
||||
|
||||
class SocketSessionManager(object):
|
||||
socket_sessions = []
|
||||
socket_session_token_key_map = {}
|
||||
|
||||
@classmethod
|
||||
def _prune(cls):
|
||||
if len(cls.socket_sessions) > 1000:
|
||||
session = cls.socket_session[0]
|
||||
del cls.socket_session_token_key_map[session.token_key]
|
||||
cls.sessions = cls.socket_sessions[1:]
|
||||
def __init__(self):
|
||||
self.SESSIONS_MAX = 1000
|
||||
self.socket_sessions = []
|
||||
self.socket_session_token_key_map = {}
|
||||
|
||||
def _prune(self):
|
||||
if len(self.socket_sessions) > self.SESSIONS_MAX:
|
||||
session = self.socket_sessions[0]
|
||||
entries = self.socket_session_token_key_map[session.token_key]
|
||||
del entries[session.session_id]
|
||||
if len(entries) == 0:
|
||||
del self.socket_session_token_key_map[session.token_key]
|
||||
self.socket_sessions.pop(0)
|
||||
|
||||
'''
|
||||
Returns an dict of sessions <session_id, session>
|
||||
'''
|
||||
@classmethod
|
||||
def lookup(cls, token_key=None):
|
||||
def lookup(self, token_key=None):
|
||||
if not token_key:
|
||||
raise ValueError("token_key required")
|
||||
return cls.socket_session_token_key_map.get(token_key, None)
|
||||
return self.socket_session_token_key_map.get(token_key, None)
|
||||
|
||||
@classmethod
|
||||
def add_session(cls, session):
|
||||
cls.socket_sessions.append(session)
|
||||
entries = cls.socket_session_token_key_map.get(session.token_key, None)
|
||||
def add_session(self, session):
|
||||
self.socket_sessions.append(session)
|
||||
entries = self.socket_session_token_key_map.get(session.token_key, None)
|
||||
if not entries:
|
||||
entries = {}
|
||||
cls.socket_session_token_key_map[session.token_key] = entries
|
||||
self.socket_session_token_key_map[session.token_key] = entries
|
||||
entries[session.session_id] = session
|
||||
cls._prune()
|
||||
self._prune()
|
||||
return session
|
||||
|
||||
class SocketController(object):
|
||||
server = None
|
||||
|
||||
@classmethod
|
||||
def broadcast_packet(cls, packet):
|
||||
def __init__(self, SocketSessionManager):
|
||||
self.server = None
|
||||
self.SocketSessionManager = SocketSessionManager
|
||||
|
||||
def add_session(self, session):
|
||||
return self.SocketSessionManager.add_session(session)
|
||||
|
||||
def broadcast_packet(self, packet):
|
||||
# Broadcast message to everyone at endpoint
|
||||
# Loop over the 'raw' list of sockets (don't trust our list)
|
||||
for session_id, socket in list(cls.server.sockets.iteritems()):
|
||||
for session_id, socket in list(self.server.sockets.iteritems()):
|
||||
socket_session = socket.session.get('socket_session', None)
|
||||
if socket_session and socket_session.is_valid():
|
||||
try:
|
||||
@ -91,11 +100,10 @@ class SocketController(object):
|
||||
logger.error("Error sending client packet to %s: %s" % (str(session_id), str(packet)))
|
||||
logger.error("Error was: " + str(e))
|
||||
|
||||
@classmethod
|
||||
def send_packet(cls, packet, token_key):
|
||||
def send_packet(self, packet, token_key):
|
||||
if not token_key:
|
||||
raise ValueError("token_key is required")
|
||||
socket_sessions = SocketSessionManager.lookup(token_key=token_key)
|
||||
socket_sessions = self.SocketSessionManager.lookup(token_key=token_key)
|
||||
# We may not find the socket_session if the user disconnected
|
||||
# (it's actually more compliciated than that because of our prune logic)
|
||||
if not socket_sessions:
|
||||
@ -112,11 +120,12 @@ class SocketController(object):
|
||||
logger.error("Error sending client packet to %s: %s" % (str(socket_session.session_id), str(packet)))
|
||||
logger.error("Error was: " + str(e))
|
||||
|
||||
@classmethod
|
||||
def set_server(cls, server):
|
||||
cls.server = server
|
||||
def set_server(self, server):
|
||||
self.server = server
|
||||
return server
|
||||
|
||||
socketController = SocketController(SocketSessionManager())
|
||||
|
||||
#
|
||||
# Socket session is attached to self.session['socket_session']
|
||||
# self.session and self.socket.session point to the same dict
|
||||
@ -140,7 +149,7 @@ class TowerBaseNamespace(BaseNamespace):
|
||||
socket_session = SocketSession(self.socket.sessid, request_token, self.socket)
|
||||
if socket_session.is_db_token_valid():
|
||||
self.session['socket_session'] = socket_session
|
||||
SocketSessionManager.add_session(socket_session)
|
||||
socketController.add_session(socket_session)
|
||||
else:
|
||||
socket_session.invalidate()
|
||||
|
||||
@ -240,9 +249,9 @@ def notification_handler(server):
|
||||
|
||||
if 'token_key' in message:
|
||||
# Best practice not to send the token over the socket
|
||||
SocketController.send_packet(packet, message.pop('token_key'))
|
||||
socketController.send_packet(packet, message.pop('token_key'))
|
||||
else:
|
||||
SocketController.broadcast_packet(packet)
|
||||
socketController.broadcast_packet(packet)
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
'''
|
||||
@ -270,7 +279,7 @@ class Command(NoArgsCommand):
|
||||
logger.info('Listening on port http://0.0.0.0:' + str(socketio_listen_port))
|
||||
server = SocketIOServer(('0.0.0.0', socketio_listen_port), TowerSocket(), resource='socket.io')
|
||||
|
||||
SocketController.set_server(server)
|
||||
socketController.set_server(server)
|
||||
handler_thread = Thread(target=notification_handler, args=(server,))
|
||||
handler_thread.daemon = True
|
||||
handler_thread.start()
|
||||
|
||||
@ -170,10 +170,10 @@ def handle_work_error(self, task_id, subtasks=None):
|
||||
instance_name = ''
|
||||
if each_task['type'] == 'project_update':
|
||||
instance = ProjectUpdate.objects.get(id=each_task['id'])
|
||||
instance_name = instance.project.name
|
||||
instance_name = instance.name
|
||||
elif each_task['type'] == 'inventory_update':
|
||||
instance = InventoryUpdate.objects.get(id=each_task['id'])
|
||||
instance_name = instance.inventory_source.inventory.name
|
||||
instance_name = instance.name
|
||||
elif each_task['type'] == 'job':
|
||||
instance = Job.objects.get(id=each_task['id'])
|
||||
instance_name = instance.job_template.name
|
||||
@ -191,7 +191,7 @@ def handle_work_error(self, task_id, subtasks=None):
|
||||
if instance.celery_task_id != task_id:
|
||||
instance.status = 'failed'
|
||||
instance.failed = True
|
||||
instance.job_explanation = 'Previous Task Failed: {"task_type": "%s", "task_name": "%s", "task_id": "%s"}' % \
|
||||
instance.job_explanation = 'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % \
|
||||
(first_task_type, first_task_name, first_task_id)
|
||||
instance.save()
|
||||
instance.socketio_emit_status("failed")
|
||||
@ -614,6 +614,21 @@ class RunJob(BaseTask):
|
||||
if credential.ssh_key_data not in (None, ''):
|
||||
private_data[cred_name] = decrypt_field(credential, 'ssh_key_data') or ''
|
||||
|
||||
if job.cloud_credential and job.cloud_credential.kind == 'openstack':
|
||||
credential = job.cloud_credential
|
||||
openstack_auth = dict(auth_url=credential.host,
|
||||
username=credential.username,
|
||||
password=decrypt_field(credential, "password"),
|
||||
project_name=credential.project)
|
||||
openstack_data = {
|
||||
'clouds': {
|
||||
'devstack': {
|
||||
'auth': openstack_auth,
|
||||
},
|
||||
},
|
||||
}
|
||||
private_data['cloud_credential'] = yaml.safe_dump(openstack_data, default_flow_style=False, allow_unicode=True)
|
||||
|
||||
return private_data
|
||||
|
||||
def build_passwords(self, job, **kwargs):
|
||||
@ -689,6 +704,8 @@ class RunJob(BaseTask):
|
||||
env['VMWARE_USER'] = cloud_cred.username
|
||||
env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password')
|
||||
env['VMWARE_HOST'] = cloud_cred.host
|
||||
elif cloud_cred and cloud_cred.kind == 'openstack':
|
||||
env['OS_CLIENT_CONFIG_FILE'] = kwargs.get('private_data_files', {}).get('cloud_credential', '')
|
||||
|
||||
# Set environment variables related to scan jobs
|
||||
if job.job_type == PERM_INVENTORY_SCAN:
|
||||
|
||||
@ -28,11 +28,11 @@ from django.test.utils import override_settings
|
||||
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.backend import LDAPSettings
|
||||
from awx.main.management.commands.run_callback_receiver import CallbackReceiver
|
||||
from awx.main.management.commands.run_task_system import run_taskmanager
|
||||
from awx.main.utils import get_ansible_version
|
||||
from awx.main.task_engine import TaskEngager as LicenseWriter
|
||||
from awx.sso.backends import LDAPSettings
|
||||
|
||||
TEST_PLAYBOOK = '''- hosts: mygroup
|
||||
gather_facts: false
|
||||
|
||||
@ -7,3 +7,6 @@ from .run_fact_cache_receiver import * # noqa
|
||||
from .commands_monolithic import * # noqa
|
||||
from .cleanup_facts import * # noqa
|
||||
from .age_deleted import * # noqa
|
||||
from .remove_instance import * # noqa
|
||||
from .run_socketio_service import * # noqa
|
||||
|
||||
|
||||
39
awx/main/tests/commands/remove_instance.py
Normal file
39
awx/main/tests/commands/remove_instance.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved
|
||||
|
||||
# Python
|
||||
import uuid
|
||||
|
||||
# AWX
|
||||
from awx.main.tests.base import BaseTest
|
||||
from awx.main.tests.commands.base import BaseCommandMixin
|
||||
from awx.main.models import * # noqa
|
||||
|
||||
__all__ = ['RemoveInstanceCommandFunctionalTest']
|
||||
|
||||
class RemoveInstanceCommandFunctionalTest(BaseCommandMixin, BaseTest):
|
||||
uuids = []
|
||||
instances = []
|
||||
|
||||
def setup_instances(self):
|
||||
self.uuids = [uuid.uuid4().hex for x in range(0, 3)]
|
||||
self.instances.append(Instance(uuid=settings.SYSTEM_UUID, primary=True, hostname='127.0.0.1'))
|
||||
self.instances.append(Instance(uuid=self.uuids[0], primary=False, hostname='127.0.0.2'))
|
||||
self.instances.append(Instance(uuid=self.uuids[1], primary=False, hostname='127.0.0.3'))
|
||||
self.instances.append(Instance(uuid=self.uuids[2], primary=False, hostname='127.0.0.4'))
|
||||
for x in self.instances:
|
||||
x.save()
|
||||
|
||||
def setUp(self):
|
||||
super(RemoveInstanceCommandFunctionalTest, self).setUp()
|
||||
self.create_test_license_file()
|
||||
self.setup_instances()
|
||||
self.setup_users()
|
||||
|
||||
def test_default(self):
|
||||
self.assertEqual(Instance.objects.filter(hostname="127.0.0.2").count(), 1)
|
||||
result, stdout, stderr = self.run_command('remove_instance', hostname='127.0.0.2')
|
||||
self.assertIsNone(result)
|
||||
self.assertEqual(stdout, 'Successfully removed instance (uuid="%s",hostname="127.0.0.2",role="secondary").\n' % (self.uuids[0]))
|
||||
self.assertEqual(Instance.objects.filter(hostname="127.0.0.2").count(), 0)
|
||||
|
||||
116
awx/main/tests/commands/run_socketio_service.py
Normal file
116
awx/main/tests/commands/run_socketio_service.py
Normal file
@ -0,0 +1,116 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved
|
||||
|
||||
# Python
|
||||
from mock import MagicMock, Mock
|
||||
|
||||
# Django
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
# AWX
|
||||
from awx.fact.models.fact import * # noqa
|
||||
from awx.main.management.commands.run_socketio_service import SocketSessionManager, SocketSession, SocketController
|
||||
|
||||
__all__ = ['SocketSessionManagerUnitTest', 'SocketControllerUnitTest',]
|
||||
|
||||
class WeakRefable():
|
||||
pass
|
||||
|
||||
class SocketSessionManagerUnitTest(SimpleTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.session_manager = SocketSessionManager()
|
||||
super(SocketSessionManagerUnitTest, self).setUp()
|
||||
|
||||
def create_sessions(self, count, token_key=None):
|
||||
self.sessions = []
|
||||
self.count = count
|
||||
for i in range(0, count):
|
||||
self.sessions.append(SocketSession(i, token_key or i, WeakRefable()))
|
||||
self.session_manager.add_session(self.sessions[i])
|
||||
|
||||
def test_multiple_session_diff_token(self):
|
||||
self.create_sessions(10)
|
||||
|
||||
for s in self.sessions:
|
||||
self.assertIn(s.token_key, self.session_manager.socket_session_token_key_map)
|
||||
self.assertEqual(s, self.session_manager.socket_session_token_key_map[s.token_key][s.session_id])
|
||||
|
||||
|
||||
def test_multiple_session_same_token(self):
|
||||
self.create_sessions(10, token_key='foo')
|
||||
|
||||
sessions_dict = self.session_manager.lookup("foo")
|
||||
self.assertEqual(len(sessions_dict), 10)
|
||||
for s in self.sessions:
|
||||
self.assertIn(s.session_id, sessions_dict)
|
||||
self.assertEqual(s, sessions_dict[s.session_id])
|
||||
|
||||
def test_prune_sessions_max(self):
|
||||
self.create_sessions(self.session_manager.SESSIONS_MAX + 10)
|
||||
|
||||
self.assertEqual(len(self.session_manager.socket_sessions), self.session_manager.SESSIONS_MAX)
|
||||
|
||||
|
||||
class SocketControllerUnitTest(SimpleTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.socket_controller = SocketController(SocketSessionManager())
|
||||
server = Mock()
|
||||
self.socket_controller.set_server(server)
|
||||
super(SocketControllerUnitTest, self).setUp()
|
||||
|
||||
def create_clients(self, count, token_key=None):
|
||||
self.sessions = []
|
||||
self.sockets =[]
|
||||
self.count = count
|
||||
self.sockets_dict = {}
|
||||
for i in range(0, count):
|
||||
if isinstance(token_key, list):
|
||||
token_key_actual = token_key[i]
|
||||
else:
|
||||
token_key_actual = token_key or i
|
||||
socket = MagicMock(session=dict())
|
||||
socket_session = SocketSession(i, token_key_actual, socket)
|
||||
self.sockets.append(socket)
|
||||
self.sessions.append(socket_session)
|
||||
self.sockets_dict[i] = socket
|
||||
self.socket_controller.add_session(socket_session)
|
||||
|
||||
socket.session['socket_session'] = socket_session
|
||||
socket.send_packet = Mock()
|
||||
self.socket_controller.server.sockets = self.sockets_dict
|
||||
|
||||
def test_broadcast_packet(self):
|
||||
self.create_clients(10)
|
||||
packet = {
|
||||
"hello": "world"
|
||||
}
|
||||
self.socket_controller.broadcast_packet(packet)
|
||||
for s in self.sockets:
|
||||
s.send_packet.assert_called_with(packet)
|
||||
|
||||
def test_send_packet(self):
|
||||
self.create_clients(5, token_key=[0, 1, 2, 3, 4])
|
||||
packet = {
|
||||
"hello": "world"
|
||||
}
|
||||
self.socket_controller.send_packet(packet, 2)
|
||||
self.assertEqual(0, len(self.sockets[0].send_packet.mock_calls))
|
||||
self.assertEqual(0, len(self.sockets[1].send_packet.mock_calls))
|
||||
self.sockets[2].send_packet.assert_called_once_with(packet)
|
||||
self.assertEqual(0, len(self.sockets[3].send_packet.mock_calls))
|
||||
self.assertEqual(0, len(self.sockets[4].send_packet.mock_calls))
|
||||
|
||||
def test_send_packet_multiple_sessions_one_token(self):
|
||||
self.create_clients(5, token_key=[0, 1, 1, 1, 2])
|
||||
packet = {
|
||||
"hello": "world"
|
||||
}
|
||||
self.socket_controller.send_packet(packet, 1)
|
||||
self.assertEqual(0, len(self.sockets[0].send_packet.mock_calls))
|
||||
self.sockets[1].send_packet.assert_called_once_with(packet)
|
||||
self.sockets[2].send_packet.assert_called_once_with(packet)
|
||||
self.sockets[3].send_packet.assert_called_once_with(packet)
|
||||
self.assertEqual(0, len(self.sockets[4].send_packet.mock_calls))
|
||||
|
||||
@ -327,6 +327,12 @@ class Ec2Inventory(object):
|
||||
else:
|
||||
self.nested_groups = False
|
||||
|
||||
# Replace dash or not in group names
|
||||
if config.has_option('ec2', 'replace_dash_in_groups'):
|
||||
self.replace_dash_in_groups = config.getboolean('ec2', 'replace_dash_in_groups')
|
||||
else:
|
||||
self.replace_dash_in_groups = True
|
||||
|
||||
# Configure which groups should be created.
|
||||
group_by_options = [
|
||||
'group_by_instance_id',
|
||||
@ -360,7 +366,7 @@ class Ec2Inventory(object):
|
||||
self.pattern_include = re.compile(pattern_include)
|
||||
else:
|
||||
self.pattern_include = None
|
||||
except configparser.NoOptionError as e:
|
||||
except configparser.NoOptionError:
|
||||
self.pattern_include = None
|
||||
|
||||
# Do we need to exclude hosts that match a pattern?
|
||||
@ -370,7 +376,7 @@ class Ec2Inventory(object):
|
||||
self.pattern_exclude = re.compile(pattern_exclude)
|
||||
else:
|
||||
self.pattern_exclude = None
|
||||
except configparser.NoOptionError as e:
|
||||
except configparser.NoOptionError:
|
||||
self.pattern_exclude = None
|
||||
|
||||
# Instance filters (see boto and EC2 API docs). Ignore invalid filters.
|
||||
@ -697,7 +703,8 @@ class Ec2Inventory(object):
|
||||
self.push(self.inventory, key, dest)
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k))
|
||||
self.push_group(self.inventory, self.to_safe("tag_" + k), key)
|
||||
if v:
|
||||
self.push_group(self.inventory, self.to_safe("tag_" + k), key)
|
||||
|
||||
# Inventory: Group by Route53 domain names if enabled
|
||||
if self.route53_enabled and self.group_by_route53_names:
|
||||
@ -1285,10 +1292,11 @@ class Ec2Inventory(object):
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', temp).lower()
|
||||
|
||||
def to_safe(self, word):
|
||||
''' Converts 'bad' characters in a string to underscores so they can be
|
||||
used as Ansible groups '''
|
||||
|
||||
return re.sub("[^A-Za-z0-9\_]", "_", word)
|
||||
''' Converts 'bad' characters in a string to underscores so they can be used as Ansible groups '''
|
||||
regex = "[^A-Za-z0-9\_"
|
||||
if self.replace_dash_in_groups:
|
||||
regex += "\-"
|
||||
return re.sub(regex + "]", "_", word)
|
||||
|
||||
def json_format_dict(self, data, pretty=False):
|
||||
''' Converts a dict to a JSON object and dumps it as a formatted
|
||||
@ -1302,3 +1310,4 @@ class Ec2Inventory(object):
|
||||
|
||||
# Run the script
|
||||
Ec2Inventory()
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
# All Rights Reserved.
|
||||
|
||||
import os
|
||||
import re # noqa
|
||||
import sys
|
||||
import djcelery
|
||||
from datetime import timedelta
|
||||
@ -217,13 +218,13 @@ REST_FRAMEWORK = {
|
||||
}
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'awx.main.backend.LDAPBackend',
|
||||
'radiusauth.backends.RADIUSBackend',
|
||||
'awx.sso.backends.LDAPBackend',
|
||||
'awx.sso.backends.RADIUSBackend',
|
||||
'social.backends.google.GoogleOAuth2',
|
||||
'social.backends.github.GithubOAuth2',
|
||||
'social.backends.github.GithubOrganizationOAuth2',
|
||||
'social.backends.github.GithubTeamOAuth2',
|
||||
'social.backends.saml.SAMLAuth',
|
||||
'awx.sso.backends.SAMLAuth',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
@ -355,13 +356,15 @@ SOCIAL_AUTH_PIPELINE = (
|
||||
'social.pipeline.social_auth.social_user',
|
||||
'social.pipeline.user.get_username',
|
||||
'social.pipeline.social_auth.associate_by_email',
|
||||
'social.pipeline.mail.mail_validation',
|
||||
'social.pipeline.user.create_user',
|
||||
'awx.sso.pipeline.check_user_found_or_created',
|
||||
'social.pipeline.social_auth.associate_user',
|
||||
'social.pipeline.social_auth.load_extra_data',
|
||||
'awx.sso.pipeline.set_is_active_for_new_user',
|
||||
'social.pipeline.user.user_details',
|
||||
'awx.sso.pipeline.prevent_inactive_login',
|
||||
'awx.sso.pipeline.update_user_orgs',
|
||||
'awx.sso.pipeline.update_user_teams',
|
||||
)
|
||||
|
||||
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = ''
|
||||
@ -370,14 +373,17 @@ SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ['profile']
|
||||
|
||||
SOCIAL_AUTH_GITHUB_KEY = ''
|
||||
SOCIAL_AUTH_GITHUB_SECRET = ''
|
||||
SOCIAL_AUTH_GITHUB_SCOPE = ['user:email', 'read:org']
|
||||
|
||||
SOCIAL_AUTH_GITHUB_ORG_KEY = ''
|
||||
SOCIAL_AUTH_GITHUB_ORG_SECRET = ''
|
||||
SOCIAL_AUTH_GITHUB_ORG_NAME = ''
|
||||
SOCIAL_AUTH_GITHUB_ORG_SCOPE = ['user:email', 'read:org']
|
||||
|
||||
SOCIAL_AUTH_GITHUB_TEAM_KEY = ''
|
||||
SOCIAL_AUTH_GITHUB_TEAM_SECRET = ''
|
||||
SOCIAL_AUTH_GITHUB_TEAM_ID = ''
|
||||
SOCIAL_AUTH_GITHUB_TEAM_SCOPE = ['user:email', 'read:org']
|
||||
|
||||
SOCIAL_AUTH_SAML_SP_ENTITY_ID = ''
|
||||
SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = ''
|
||||
@ -400,6 +406,9 @@ SOCIAL_AUTH_CLEAN_USERNAMES = True
|
||||
SOCIAL_AUTH_SANITIZE_REDIRECTS = True
|
||||
SOCIAL_AUTH_REDIRECT_IS_HTTPS = False
|
||||
|
||||
SOCIAL_AUTH_ORGANIZATION_MAP = {}
|
||||
SOCIAL_AUTH_TEAM_MAP = {}
|
||||
|
||||
# Any ANSIBLE_* settings will be passed to the subprocess environment by the
|
||||
# celery task.
|
||||
|
||||
@ -572,13 +581,19 @@ VMWARE_EXCLUDE_EMPTY_GROUPS = True
|
||||
# provide a list here.
|
||||
# Source: https://developers.google.com/compute/docs/zones
|
||||
GCE_REGION_CHOICES = [
|
||||
('us-east1-b', 'US East (B)'),
|
||||
('us-east1-c', 'US East (C)'),
|
||||
('us-east1-d', 'US East (D)'),
|
||||
('us-central1-a', 'US Central (A)'),
|
||||
('us-central1-b', 'US Central (B)'),
|
||||
('us-central1-c', 'US Central (C)'),
|
||||
('us-central1-f', 'US Central (F)'),
|
||||
('europe-west1-a', 'Europe West (A)'),
|
||||
('europe-west1-b', 'Europe West (B)'),
|
||||
('europe-west1-c', 'Europe West (C)'),
|
||||
('europe-west1-d', 'Europe West (D)'),
|
||||
('asia-east1-a', 'Asia East (A)'),
|
||||
('asia-east1-b', 'Asia East (B)'),
|
||||
('asia-east1-c', 'Asia East (C)'),
|
||||
]
|
||||
GCE_REGIONS_BLACKLIST = []
|
||||
|
||||
@ -715,7 +730,7 @@ LOGGING = {
|
||||
'level': 'WARNING',
|
||||
'class':'logging.handlers.RotatingFileHandler',
|
||||
'filters': ['require_debug_false'],
|
||||
'filename': os.path.join(LOG_ROOT, 'tower_warnings.log'),
|
||||
'filename': os.path.join(LOG_ROOT, 'tower.log'),
|
||||
'maxBytes': 1024 * 1024 * 5, # 5 MB
|
||||
'backupCount': 5,
|
||||
'formatter':'simple',
|
||||
@ -807,7 +822,12 @@ LOGGING = {
|
||||
'propagate': False,
|
||||
},
|
||||
'django_auth_ldap': {
|
||||
'handlers': ['null'],
|
||||
'handlers': ['console', 'file', 'tower_warnings'],
|
||||
'level': 'DEBUG',
|
||||
},
|
||||
'social': {
|
||||
'handlers': ['console', 'file', 'tower_warnings'],
|
||||
'level': 'DEBUG',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -525,8 +525,80 @@ SOCIAL_AUTH_SAML_ENABLED_IDPS = {
|
||||
# 'url': 'https://myidp.example.com/sso',
|
||||
# 'x509cert': '',
|
||||
#},
|
||||
#'onelogin': {
|
||||
# 'entity_id': 'https://app.onelogin.com/saml/metadata/123456',
|
||||
# 'url': 'https://example.onelogin.com/trust/saml2/http-post/sso/123456',
|
||||
# 'x509cert': '',
|
||||
# 'attr_user_permanent_id': 'name_id',
|
||||
# 'attr_first_name': 'User.FirstName',
|
||||
# 'attr_last_name': 'User.LastName',
|
||||
# 'attr_username': 'User.email',
|
||||
# 'attr_email': 'User.email',
|
||||
#},
|
||||
}
|
||||
|
||||
SOCIAL_AUTH_ORGANIZATION_MAP = {
|
||||
# Add all users to the default organization.
|
||||
'Default': {
|
||||
'users': True,
|
||||
},
|
||||
#'Test Org': {
|
||||
# 'admins': ['admin@example.com'],
|
||||
# 'users': True,
|
||||
#},
|
||||
#'Test Org 2': {
|
||||
# 'admins': ['admin@example.com', re.compile(r'^tower-[^@]+*?@.*$],
|
||||
# 'users': re.compile(r'^[^@].*?@example\.com$'),
|
||||
#},
|
||||
}
|
||||
|
||||
#SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP = {}
|
||||
#SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP = {}
|
||||
#SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP = {}
|
||||
#SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP = {}
|
||||
#SOCIAL_AUTH_SAML_ORGANIZATION_MAP = {}
|
||||
|
||||
SOCIAL_AUTH_TEAM_MAP = {
|
||||
#'My Team': {
|
||||
# 'organization': 'Test Org',
|
||||
# 'users': ['re.compile(r'^[^@]+?@test\.example\.com$')'],
|
||||
# 'remove': True,
|
||||
#},
|
||||
#'Other Team': {
|
||||
# 'organization': 'Test Org 2',
|
||||
# 'users': re.compile(r'^[^@]+?@test2\.example\.com$'),
|
||||
# 'remove': False,
|
||||
#},
|
||||
}
|
||||
|
||||
#SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP = {}
|
||||
#SOCIAL_AUTH_GITHUB_TEAM_MAP = {}
|
||||
#SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP = {}
|
||||
#SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP = {}
|
||||
#SOCIAL_AUTH_SAML_TEAM_MAP = {}
|
||||
|
||||
# Uncomment one or more of the lines below to prevent new user accounts from
|
||||
# being created for the selected social auth providers. Only users who have
|
||||
# previously logged in using social auth or have a user account with a matching
|
||||
# email address will be able to login.
|
||||
|
||||
#SOCIAL_AUTH_USER_FIELDS = []
|
||||
#SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = []
|
||||
#SOCIAL_AUTH_GITHUB_USER_FIELDS = []
|
||||
#SOCIAL_AUTH_GITHUB_ORG_USER_FIELDS = []
|
||||
#SOCIAL_AUTH_GITHUB_TEAM_USER_FIELDS = []
|
||||
#SOCIAL_AUTH_SAML_USER_FIELDS = []
|
||||
|
||||
# It is also possible to add custom functions to the social auth pipeline for
|
||||
# more advanced organization and team mapping. Use at your own risk.
|
||||
|
||||
#def custom_social_auth_pipeline_function(backend, details, user=None, *args, **kwargs):
|
||||
# print 'custom:', backend, details, user, args, kwargs
|
||||
|
||||
#SOCIAL_AUTH_PIPELINE += (
|
||||
# 'awx.settings.development.custom_social_auth_pipeline_function',
|
||||
#)
|
||||
|
||||
###############################################################################
|
||||
# INVENTORY IMPORT TEST SETTINGS
|
||||
###############################################################################
|
||||
|
||||
@ -523,8 +523,80 @@ SOCIAL_AUTH_SAML_ENABLED_IDPS = {
|
||||
# 'url': 'https://myidp.example.com/sso',
|
||||
# 'x509cert': '',
|
||||
#},
|
||||
#'onelogin': {
|
||||
# 'entity_id': 'https://app.onelogin.com/saml/metadata/123456',
|
||||
# 'url': 'https://example.onelogin.com/trust/saml2/http-post/sso/123456',
|
||||
# 'x509cert': '',
|
||||
# 'attr_user_permanent_id': 'name_id',
|
||||
# 'attr_first_name': 'User.FirstName',
|
||||
# 'attr_last_name': 'User.LastName',
|
||||
# 'attr_username': 'User.email',
|
||||
# 'attr_email': 'User.email',
|
||||
#},
|
||||
}
|
||||
|
||||
SOCIAL_AUTH_ORGANIZATION_MAP = {
|
||||
# Add all users to the default organization.
|
||||
'Default': {
|
||||
'users': True,
|
||||
},
|
||||
#'Test Org': {
|
||||
# 'admins': ['admin@example.com'],
|
||||
# 'users': True,
|
||||
#},
|
||||
#'Test Org 2': {
|
||||
# 'admins': ['admin@example.com', re.compile(r'^tower-[^@]+*?@.*$],
|
||||
# 'users': re.compile(r'^[^@].*?@example\.com$'),
|
||||
#},
|
||||
}
|
||||
|
||||
#SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP = {}
|
||||
#SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP = {}
|
||||
#SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP = {}
|
||||
#SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP = {}
|
||||
#SOCIAL_AUTH_SAML_ORGANIZATION_MAP = {}
|
||||
|
||||
SOCIAL_AUTH_TEAM_MAP = {
|
||||
#'My Team': {
|
||||
# 'organization': 'Test Org',
|
||||
# 'users': ['re.compile(r'^[^@]+?@test\.example\.com$')'],
|
||||
# 'remove': True,
|
||||
#},
|
||||
#'Other Team': {
|
||||
# 'organization': 'Test Org 2',
|
||||
# 'users': re.compile(r'^[^@]+?@test2\.example\.com$'),
|
||||
# 'remove': False,
|
||||
#},
|
||||
}
|
||||
|
||||
#SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP = {}
|
||||
#SOCIAL_AUTH_GITHUB_TEAM_MAP = {}
|
||||
#SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP = {}
|
||||
#SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP = {}
|
||||
#SOCIAL_AUTH_SAML_TEAM_MAP = {}
|
||||
|
||||
# Uncomment one or more of the lines below to prevent new user accounts from
|
||||
# being created for the selected social auth providers. Only users who have
|
||||
# previously logged in using social auth or have a user account with a matching
|
||||
# email address will be able to login.
|
||||
|
||||
#SOCIAL_AUTH_USER_FIELDS = []
|
||||
#SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = []
|
||||
#SOCIAL_AUTH_GITHUB_USER_FIELDS = []
|
||||
#SOCIAL_AUTH_GITHUB_ORG_USER_FIELDS = []
|
||||
#SOCIAL_AUTH_GITHUB_TEAM_USER_FIELDS = []
|
||||
#SOCIAL_AUTH_SAML_USER_FIELDS = []
|
||||
|
||||
# It is also possible to add custom functions to the social auth pipeline for
|
||||
# more advanced organization and team mapping. Use at your own risk.
|
||||
|
||||
#def custom_social_auth_pipeline_function(backend, details, user=None, *args, **kwargs):
|
||||
# print 'custom:', backend, details, user, args, kwargs
|
||||
|
||||
#SOCIAL_AUTH_PIPELINE += (
|
||||
# 'awx.settings.development.custom_social_auth_pipeline_function',
|
||||
#)
|
||||
|
||||
###############################################################################
|
||||
# INVENTORY IMPORT TEST SETTINGS
|
||||
###############################################################################
|
||||
|
||||
@ -7,10 +7,10 @@
|
||||
# settings as needed.
|
||||
|
||||
if not AUTH_LDAP_SERVER_URI:
|
||||
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.main.backend.LDAPBackend']
|
||||
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.sso.backends.LDAPBackend']
|
||||
|
||||
if not RADIUS_SERVER:
|
||||
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'radiusauth.backends.RADIUSBackend']
|
||||
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.sso.backends.RADIUSBackend']
|
||||
|
||||
if not all([SOCIAL_AUTH_GOOGLE_OAUTH2_KEY, SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET]):
|
||||
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'social.backends.google.GoogleOAuth2']
|
||||
@ -28,8 +28,7 @@ if not all([SOCIAL_AUTH_SAML_SP_ENTITY_ID, SOCIAL_AUTH_SAML_SP_PUBLIC_CERT,
|
||||
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY, SOCIAL_AUTH_SAML_ORG_INFO,
|
||||
SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, SOCIAL_AUTH_SAML_SUPPORT_CONTACT,
|
||||
SOCIAL_AUTH_SAML_ENABLED_IDPS]):
|
||||
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'social.backends.saml.SAMLAuth']
|
||||
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.sso.backends.SAMLAuth']
|
||||
|
||||
if not AUTH_BASIC_ENABLED:
|
||||
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = [x for x in REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] if x != 'rest_framework.authentication.BasicAuthentication']
|
||||
|
||||
|
||||
232
awx/sso/backends.py
Normal file
232
awx/sso/backends.py
Normal file
@ -0,0 +1,232 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
# Python
|
||||
import logging
|
||||
|
||||
# Django
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings as django_settings
|
||||
|
||||
# django-auth-ldap
|
||||
from django_auth_ldap.backend import LDAPSettings as BaseLDAPSettings
|
||||
from django_auth_ldap.backend import LDAPBackend as BaseLDAPBackend
|
||||
from django_auth_ldap.backend import populate_user
|
||||
|
||||
# radiusauth
|
||||
from radiusauth.backends import RADIUSBackend as BaseRADIUSBackend
|
||||
|
||||
# social
|
||||
from social.backends.saml import OID_USERID
|
||||
from social.backends.saml import SAMLAuth as BaseSAMLAuth
|
||||
from social.backends.saml import SAMLIdentityProvider as BaseSAMLIdentityProvider
|
||||
|
||||
# Ansible Tower
|
||||
from awx.api.license import feature_enabled
|
||||
|
||||
logger = logging.getLogger('awx.sso.backends')
|
||||
|
||||
|
||||
class LDAPSettings(BaseLDAPSettings):
|
||||
|
||||
defaults = dict(BaseLDAPSettings.defaults.items() + {
|
||||
'ORGANIZATION_MAP': {},
|
||||
'TEAM_MAP': {},
|
||||
}.items())
|
||||
|
||||
|
||||
class LDAPBackend(BaseLDAPBackend):
|
||||
'''
|
||||
Custom LDAP backend for AWX.
|
||||
'''
|
||||
|
||||
settings_prefix = 'AUTH_LDAP_'
|
||||
|
||||
def _get_settings(self):
|
||||
if self._settings is None:
|
||||
self._settings = LDAPSettings(self.settings_prefix)
|
||||
return self._settings
|
||||
|
||||
def _set_settings(self, settings):
|
||||
self._settings = settings
|
||||
|
||||
settings = property(_get_settings, _set_settings)
|
||||
|
||||
def authenticate(self, username, password):
|
||||
if not self.settings.SERVER_URI:
|
||||
return None
|
||||
if not feature_enabled('ldap'):
|
||||
logger.error("Unable to authenticate, license does not support LDAP authentication")
|
||||
return None
|
||||
return super(LDAPBackend, self).authenticate(username, password)
|
||||
|
||||
def get_user(self, user_id):
|
||||
if not self.settings.SERVER_URI:
|
||||
return None
|
||||
if not feature_enabled('ldap'):
|
||||
logger.error("Unable to get_user, license does not support LDAP authentication")
|
||||
return None
|
||||
return super(LDAPBackend, self).get_user(user_id)
|
||||
|
||||
# Disable any LDAP based authorization / permissions checking.
|
||||
|
||||
def has_perm(self, user, perm, obj=None):
|
||||
return False
|
||||
|
||||
def has_module_perms(self, user, app_label):
|
||||
return False
|
||||
|
||||
def get_all_permissions(self, user, obj=None):
|
||||
return set()
|
||||
|
||||
def get_group_permissions(self, user, obj=None):
|
||||
return set()
|
||||
|
||||
|
||||
class RADIUSBackend(BaseRADIUSBackend):
|
||||
'''
|
||||
Custom Radius backend to verify license status
|
||||
'''
|
||||
|
||||
def authenticate(self, username, password):
|
||||
if not django_settings.RADIUS_SERVER:
|
||||
return None
|
||||
if not feature_enabled('enterprise_auth'):
|
||||
logger.error("Unable to authenticate, license does not support RADIUS authentication")
|
||||
return None
|
||||
return super(RADIUSBackend, self).authenticate(username, password)
|
||||
|
||||
def get_user(self, user_id):
|
||||
if not django_settings.RADIUS_SERVER:
|
||||
return None
|
||||
if not feature_enabled('enterprise_auth'):
|
||||
logger.error("Unable to get_user, license does not support RADIUS authentication")
|
||||
return None
|
||||
return super(RADIUSBackend, self).get_user(user_id)
|
||||
|
||||
|
||||
class TowerSAMLIdentityProvider(BaseSAMLIdentityProvider):
|
||||
'''
|
||||
Custom Identity Provider to make attributes to what we expect.
|
||||
'''
|
||||
|
||||
def get_user_permanent_id(self, attributes):
|
||||
uid = attributes[self.conf.get('attr_user_permanent_id', OID_USERID)]
|
||||
if isinstance(uid, basestring):
|
||||
return uid
|
||||
return uid[0]
|
||||
|
||||
def get_attr(self, attributes, conf_key, default_attribute):
|
||||
"""
|
||||
Get the attribute 'default_attribute' out of the attributes,
|
||||
unless self.conf[conf_key] overrides the default by specifying
|
||||
another attribute to use.
|
||||
"""
|
||||
key = self.conf.get(conf_key, default_attribute)
|
||||
value = attributes[key][0] if key in attributes else None
|
||||
if conf_key in ('attr_first_name', 'attr_last_name', 'attr_username', 'attr_email') and value is None:
|
||||
logger.warn("Could not map user detail '%s' from SAML attribute '%s'; "
|
||||
"update SOCIAL_AUTH_SAML_ENABLED_IDPS['%s']['%s'] with the correct SAML attribute.",
|
||||
conf_key[5:], key, self.name, conf_key)
|
||||
return unicode(value) if value is not None else value
|
||||
|
||||
|
||||
class SAMLAuth(BaseSAMLAuth):
|
||||
'''
|
||||
Custom SAMLAuth backend to verify license status
|
||||
'''
|
||||
|
||||
def get_idp(self, idp_name):
|
||||
idp_config = self.setting('ENABLED_IDPS')[idp_name]
|
||||
return TowerSAMLIdentityProvider(idp_name, **idp_config)
|
||||
|
||||
def authenticate(self, *args, **kwargs):
|
||||
if not all([django_settings.SOCIAL_AUTH_SAML_SP_ENTITY_ID, django_settings.SOCIAL_AUTH_SAML_SP_PUBLIC_CERT,
|
||||
django_settings.SOCIAL_AUTH_SAML_SP_PRIVATE_KEY, django_settings.SOCIAL_AUTH_SAML_ORG_INFO,
|
||||
django_settings.SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, django_settings.SOCIAL_AUTH_SAML_SUPPORT_CONTACT,
|
||||
django_settings.SOCIAL_AUTH_SAML_ENABLED_IDPS]):
|
||||
return None
|
||||
if not feature_enabled('enterprise_auth'):
|
||||
logger.error("Unable to authenticate, license does not support SAML authentication")
|
||||
return None
|
||||
return super(SAMLAuth, self).authenticate(*args, **kwargs)
|
||||
|
||||
def get_user(self, user_id):
|
||||
if not all([django_settings.SOCIAL_AUTH_SAML_SP_ENTITY_ID, django_settings.SOCIAL_AUTH_SAML_SP_PUBLIC_CERT,
|
||||
django_settings.SOCIAL_AUTH_SAML_SP_PRIVATE_KEY, django_settings.SOCIAL_AUTH_SAML_ORG_INFO,
|
||||
django_settings.SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, django_settings.SOCIAL_AUTH_SAML_SUPPORT_CONTACT,
|
||||
django_settings.SOCIAL_AUTH_SAML_ENABLED_IDPS]):
|
||||
return None
|
||||
if not feature_enabled('enterprise_auth'):
|
||||
logger.error("Unable to get_user, license does not support SAML authentication")
|
||||
return None
|
||||
return super(SAMLAuth, self).get_user(user_id)
|
||||
|
||||
|
||||
def _update_m2m_from_groups(user, ldap_user, rel, opts, remove=False):
|
||||
'''
|
||||
Hepler function to update m2m relationship based on LDAP group membership.
|
||||
'''
|
||||
should_add = False
|
||||
if opts is None:
|
||||
return
|
||||
elif not opts:
|
||||
pass
|
||||
elif opts is True:
|
||||
should_add = True
|
||||
else:
|
||||
if isinstance(opts, basestring):
|
||||
opts = [opts]
|
||||
for group_dn in opts:
|
||||
if not isinstance(group_dn, basestring):
|
||||
continue
|
||||
if ldap_user._get_groups().is_member_of(group_dn):
|
||||
should_add = True
|
||||
if should_add:
|
||||
rel.add(user)
|
||||
elif remove:
|
||||
rel.remove(user)
|
||||
|
||||
|
||||
@receiver(populate_user)
|
||||
def on_populate_user(sender, **kwargs):
|
||||
'''
|
||||
Handle signal from LDAP backend to populate the user object. Update user
|
||||
organization/team memberships according to their LDAP groups.
|
||||
'''
|
||||
from awx.main.models import Organization, Team
|
||||
user = kwargs['user']
|
||||
ldap_user = kwargs['ldap_user']
|
||||
backend = ldap_user.backend
|
||||
|
||||
# Update organization membership based on group memberships.
|
||||
org_map = getattr(backend.settings, 'ORGANIZATION_MAP', {})
|
||||
for org_name, org_opts in org_map.items():
|
||||
org, created = Organization.objects.get_or_create(name=org_name)
|
||||
remove = bool(org_opts.get('remove', False))
|
||||
admins_opts = org_opts.get('admins', None)
|
||||
remove_admins = bool(org_opts.get('remove_admins', remove))
|
||||
_update_m2m_from_groups(user, ldap_user, org.admins, admins_opts,
|
||||
remove_admins)
|
||||
users_opts = org_opts.get('users', None)
|
||||
remove_users = bool(org_opts.get('remove_users', remove))
|
||||
_update_m2m_from_groups(user, ldap_user, org.users, users_opts,
|
||||
remove_users)
|
||||
|
||||
# Update team membership based on group memberships.
|
||||
team_map = getattr(backend.settings, 'TEAM_MAP', {})
|
||||
for team_name, team_opts in team_map.items():
|
||||
if 'organization' not in team_opts:
|
||||
continue
|
||||
org, created = Organization.objects.get_or_create(name=team_opts['organization'])
|
||||
team, created = Team.objects.get_or_create(name=team_name, organization=org)
|
||||
users_opts = team_opts.get('users', None)
|
||||
remove = bool(team_opts.get('remove', False))
|
||||
_update_m2m_from_groups(user, ldap_user, team.users, users_opts,
|
||||
remove)
|
||||
|
||||
# Update user profile to store LDAP DN.
|
||||
profile = user.profile
|
||||
if profile.ldap_dn != ldap_user.dn:
|
||||
profile.ldap_dn = ldap_user.dn
|
||||
profile.save()
|
||||
@ -17,13 +17,13 @@ from social.exceptions import SocialAuthBaseException
|
||||
from social.utils import social_logger
|
||||
from social.apps.django_app.middleware import SocialAuthExceptionMiddleware
|
||||
|
||||
# Ansible Tower
|
||||
from awx.main.models import AuthToken
|
||||
|
||||
|
||||
class SocialAuthMiddleware(SocialAuthExceptionMiddleware):
|
||||
|
||||
def process_request(self, request):
|
||||
request.META['SERVER_PORT'] = 80 # FIXME
|
||||
|
||||
token_key = request.COOKIES.get('token', '')
|
||||
token_key = urllib.quote(urllib.unquote(token_key).strip('"'))
|
||||
|
||||
@ -63,7 +63,7 @@ class SocialAuthMiddleware(SocialAuthExceptionMiddleware):
|
||||
if strategy is None or self.raise_exception(request, exception):
|
||||
return
|
||||
|
||||
if isinstance(exception, SocialAuthBaseException):
|
||||
if isinstance(exception, SocialAuthBaseException) or request.path.startswith('/sso/'):
|
||||
backend = getattr(request, 'backend', None)
|
||||
backend_name = getattr(backend, 'name', 'unknown-backend')
|
||||
full_backend_name = backend_name
|
||||
|
||||
@ -1,17 +1,38 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
# Python
|
||||
import re
|
||||
|
||||
# Python Social Auth
|
||||
from social.exceptions import AuthException
|
||||
|
||||
# Tower
|
||||
from awx.api.license import feature_enabled
|
||||
|
||||
|
||||
class AuthNotFound(AuthException):
|
||||
|
||||
def __init__(self, backend, email_or_uid, *args, **kwargs):
|
||||
self.email_or_uid = email_or_uid
|
||||
super(AuthNotFound, self).__init__(backend, *args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return 'An account cannot be found for {0}'.format(self.email_or_uid)
|
||||
|
||||
|
||||
class AuthInactive(AuthException):
|
||||
"""Authentication for this user is forbidden"""
|
||||
|
||||
def __str__(self):
|
||||
return 'Your account is inactive'
|
||||
|
||||
|
||||
def check_user_found_or_created(backend, details, user=None, *args, **kwargs):
|
||||
if not user:
|
||||
email_or_uid = details.get('email') or kwargs.get('email') or kwargs.get('uid') or '???'
|
||||
raise AuthNotFound(backend, email_or_uid)
|
||||
|
||||
|
||||
def set_is_active_for_new_user(strategy, details, user=None, *args, **kwargs):
|
||||
if kwargs.get('is_new', False):
|
||||
details['is_active'] = True
|
||||
@ -21,3 +42,96 @@ def set_is_active_for_new_user(strategy, details, user=None, *args, **kwargs):
|
||||
def prevent_inactive_login(backend, details, user=None, *args, **kwargs):
|
||||
if user and not user.is_active:
|
||||
raise AuthInactive(backend)
|
||||
|
||||
|
||||
def _update_m2m_from_expression(user, rel, expr, remove=False):
|
||||
'''
|
||||
Helper function to update m2m relationship based on user matching one or
|
||||
more expressions.
|
||||
'''
|
||||
should_add = False
|
||||
if expr is None:
|
||||
return
|
||||
elif not expr:
|
||||
pass
|
||||
elif expr is True:
|
||||
should_add = True
|
||||
else:
|
||||
if isinstance(expr, (basestring, type(re.compile('')))):
|
||||
expr = [expr]
|
||||
for ex in expr:
|
||||
if isinstance(ex, basestring):
|
||||
if user.username == ex or user.email == ex:
|
||||
should_add = True
|
||||
elif isinstance(ex, type(re.compile(''))):
|
||||
if ex.match(user.username) or ex.match(user.email):
|
||||
should_add = True
|
||||
if should_add:
|
||||
rel.add(user)
|
||||
elif remove:
|
||||
rel.remove(user)
|
||||
|
||||
|
||||
def update_user_orgs(backend, details, user=None, *args, **kwargs):
|
||||
'''
|
||||
Update organization memberships for the given user based on mapping rules
|
||||
defined in settings.
|
||||
'''
|
||||
if not user:
|
||||
return
|
||||
from awx.main.models import Organization
|
||||
multiple_orgs = feature_enabled('multiple_organizations')
|
||||
org_map = backend.setting('ORGANIZATION_MAP') or {}
|
||||
for org_name, org_opts in org_map.items():
|
||||
|
||||
# Get or create the org to update. If the license only allows for one
|
||||
# org, always use the first active org, unless no org exists.
|
||||
if multiple_orgs:
|
||||
org = Organization.objects.get_or_create(name=org_name)[0]
|
||||
else:
|
||||
try:
|
||||
org = Organization.objects.filter(active=True).order_by('pk')[0]
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
# Update org admins from expression(s).
|
||||
remove = bool(org_opts.get('remove', False))
|
||||
admins_expr = org_opts.get('admins', None)
|
||||
remove_admins = bool(org_opts.get('remove_admins', remove))
|
||||
_update_m2m_from_expression(user, org.admins, admins_expr, remove_admins)
|
||||
|
||||
# Update org users from expression(s).
|
||||
users_expr = org_opts.get('users', None)
|
||||
remove_users = bool(org_opts.get('remove_users', remove))
|
||||
_update_m2m_from_expression(user, org.users, users_expr, remove_users)
|
||||
|
||||
|
||||
def update_user_teams(backend, details, user=None, *args, **kwargs):
|
||||
'''
|
||||
Update team memberships for the given user based on mapping rules defined
|
||||
in settings.
|
||||
'''
|
||||
if not user:
|
||||
return
|
||||
from awx.main.models import Organization, Team
|
||||
multiple_orgs = feature_enabled('multiple_organizations')
|
||||
team_map = backend.setting('TEAM_MAP') or {}
|
||||
for team_name, team_opts in team_map.items():
|
||||
|
||||
# Get or create the org to update. If the license only allows for one
|
||||
# org, always use the first active org, unless no org exists.
|
||||
if multiple_orgs:
|
||||
if 'organization' not in team_opts:
|
||||
continue
|
||||
org = Organization.objects.get_or_create(name=team_opts['organization'])[0]
|
||||
else:
|
||||
try:
|
||||
org = Organization.objects.filter(active=True).order_by('pk')[0]
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
# Update team members from expression(s).
|
||||
team = Team.objects.get_or_create(name=team_name, organization=org)[0]
|
||||
users_expr = team_opts.get('users', None)
|
||||
remove = bool(team_opts.get('remove', False))
|
||||
_update_m2m_from_expression(user, team.users, users_expr, remove)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
|
||||
<div id="about-dialog-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-5">
|
||||
<div class="col-xs-12 col-sm-5 About-cowsay">
|
||||
<div style="width: 340px; margin: 0 auto;">
|
||||
<pre id="cowsay">
|
||||
________________
|
||||
@ -19,10 +19,11 @@
|
||||
<div class="col-xs-12 col-sm-7 text-center">
|
||||
<img id="about-modal-titlelogo" src="/static/assets/ansible_tower_logo_minimalc.png"><br>
|
||||
<p>Copyright 2015. All rights reserved.</p>
|
||||
<p>Ansible and Ansible Tower are registered trademarks of Ansible, Inc.</p>
|
||||
<p>Ansible and Ansible Tower are registered trademarks of Red Hat, Inc.</p>
|
||||
<br>
|
||||
<img class="About-redhat" src="/static/assets/redhat_ansible_lockup.png">
|
||||
<br>
|
||||
<p>Visit <a href="http://www.ansible.com" target="_blank">Ansible.com</a> for more information.</p>
|
||||
<br>
|
||||
<p><span id='about-modal-subscription'></span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
14
awx/ui/client/src/about/about.block.less
Normal file
14
awx/ui/client/src/about/about.block.less
Normal file
@ -0,0 +1,14 @@
|
||||
/** @define About */
|
||||
.About {
|
||||
height: 309px !important;
|
||||
}
|
||||
|
||||
.About-cowsay {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.About-redhat {
|
||||
max-width: 100%;
|
||||
margin-top: -61px;
|
||||
margin-bottom: -33px;
|
||||
}
|
||||
@ -837,9 +837,9 @@ var tower = angular.module('Tower', [
|
||||
}]);
|
||||
}])
|
||||
|
||||
.run(['$compile', '$cookieStore', '$rootScope', '$log', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'HideStream', 'Socket',
|
||||
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'HideStream', 'Socket',
|
||||
'LoadConfig', 'Store', 'ShowSocketHelp', 'AboutAnsibleHelp', 'pendoService',
|
||||
function ($compile, $cookieStore, $rootScope, $log, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, HideStream, Socket,
|
||||
function ($q, $compile, $cookieStore, $rootScope, $log, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, HideStream, Socket,
|
||||
LoadConfig, Store, ShowSocketHelp, AboutAnsibleHelp, pendoService) {
|
||||
|
||||
|
||||
@ -950,7 +950,8 @@ var tower = angular.module('Tower', [
|
||||
control_socket.init();
|
||||
control_socket.on("limit_reached", function(data) {
|
||||
$log.debug(data.reason);
|
||||
Timer.expireSession('session_limit');
|
||||
$rootScope.sessionTimer.expireSession('session_limit');
|
||||
$location.url('/login');
|
||||
});
|
||||
}
|
||||
openSocket();
|
||||
@ -999,7 +1000,7 @@ var tower = angular.module('Tower', [
|
||||
// gets here on timeout
|
||||
if (next.templateUrl !== (urlPrefix + 'login/loginBackDrop.partial.html')) {
|
||||
$rootScope.sessionTimer.expireSession('idle');
|
||||
if (sock) {
|
||||
if (sock&& sock.socket && sock.socket.socket) {
|
||||
sock.socket.socket.disconnect();
|
||||
}
|
||||
$location.path('/login');
|
||||
@ -1026,9 +1027,11 @@ var tower = angular.module('Tower', [
|
||||
$rootScope.user_is_superuser = Authorization.getUserInfo('is_superuser');
|
||||
// when the user refreshes we want to open the socket, except if the user is on the login page, which should happen after the user logs in (see the AuthService module for that call to OpenSocket)
|
||||
if(!_.contains($location.$$url, '/login')){
|
||||
$rootScope.sessionTimer = Timer.init();
|
||||
$rootScope.$emit('OpenSocket');
|
||||
pendoService.issuePendoIdentity();
|
||||
Timer.init().then(function(timer){
|
||||
$rootScope.sessionTimer = timer;
|
||||
$rootScope.$emit('OpenSocket');
|
||||
pendoService.issuePendoIdentity();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1063,8 +1066,8 @@ var tower = angular.module('Tower', [
|
||||
|
||||
|
||||
if (!$AnsibleConfig) {
|
||||
// there may be time lag when loading the config file, so temporarily use what's in local storage
|
||||
$AnsibleConfig = Store('AnsibleConfig');
|
||||
// create a promise that will resolve when $AnsibleConfig is loaded
|
||||
$rootScope.loginConfig = $q.defer();
|
||||
}
|
||||
|
||||
//the authorization controller redirects to the home page automatcially if there is no last path defined. in order to override
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
export function JobDetailController ($location, $rootScope, $filter, $scope, $compile, $routeParams, $log, ClearScope, Breadcrumbs, LoadBreadCrumbs, GetBasePath, Wait, Rest,
|
||||
ProcessErrors, SelectPlay, SelectTask, Socket, GetElapsed, DrawGraph, LoadHostSummary, ReloadHostSummaryList, JobIsFinished, SetTaskStyles, DigestEvent,
|
||||
UpdateDOM, EventViewer, DeleteJob, PlaybookRun, HostEventsViewer, LoadPlays, LoadTasks, LoadHosts, HostsEdit, ParseVariableString, GetChoices) {
|
||||
UpdateDOM, EventViewer, DeleteJob, PlaybookRun, HostEventsViewer, LoadPlays, LoadTasks, LoadHosts, HostsEdit, ParseVariableString, GetChoices, fieldChoices, fieldLabels) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
@ -27,11 +27,33 @@ export function JobDetailController ($location, $rootScope, $filter, $scope, $co
|
||||
|
||||
scope.plays = [];
|
||||
|
||||
scope.previousTaskFailed = false;
|
||||
|
||||
scope.$watch('job_status', function(job_status) {
|
||||
if (job_status && job_status.explanation && job_status.explanation.split(":")[0] === "Previous Task Failed") {
|
||||
scope.previousTaskFailed = true;
|
||||
var taskObj = JSON.parse(job_status.explanation.substring(job_status.explanation.split(":")[0].length + 1));
|
||||
job_status.explanation = job_status.explanation.split(":")[0] + ". ";
|
||||
job_status.explanation += "<code>" + taskObj.task_type + "-" + taskObj.task_id + " failed for " + taskObj.task_name + "</code>"
|
||||
// return a promise from the options request with the permission type choices (including adhoc) as a param
|
||||
var fieldChoice = fieldChoices({
|
||||
scope: $scope,
|
||||
url: 'api/v1/unified_jobs/',
|
||||
field: 'type'
|
||||
});
|
||||
|
||||
// manipulate the choices from the options request to be set on
|
||||
// scope and be usable by the list form
|
||||
fieldChoice.then(function (choices) {
|
||||
choices =
|
||||
fieldLabels({
|
||||
choices: choices
|
||||
});
|
||||
scope.explanation_fail_type = choices[taskObj.job_type];
|
||||
scope.explanation_fail_name = taskObj.job_name;
|
||||
scope.explanation_fail_id = taskObj.job_id;
|
||||
scope.task_detail = scope.explanation_fail_type + " failed for " + scope.explanation_fail_name + " with ID " + scope.explanation_fail_id + ".";
|
||||
});
|
||||
} else {
|
||||
scope.previousTaskFailed = false;
|
||||
}
|
||||
}, true);
|
||||
|
||||
@ -649,61 +671,6 @@ export function JobDetailController ($location, $rootScope, $filter, $scope, $co
|
||||
});
|
||||
|
||||
|
||||
if (scope.removeGetCredentialNames) {
|
||||
scope.removeGetCredentialNames();
|
||||
}
|
||||
scope.removeGetCredentialNames = scope.$on('GetCredentialNames', function(e, data) {
|
||||
var url;
|
||||
if (data.credential) {
|
||||
url = GetBasePath('credentials') + data.credential + '/';
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success( function(data) {
|
||||
scope.credential_name = data.name;
|
||||
})
|
||||
.error( function(data, status) {
|
||||
scope.credential_name = '';
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to ' + url + '. GET returned: ' + status });
|
||||
});
|
||||
}
|
||||
if (data.cloud_credential) {
|
||||
url = GetBasePath('credentials') + data.cloud_credential + '/';
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success( function(data) {
|
||||
scope.cloud_credential_name = data.name;
|
||||
})
|
||||
.error( function(data, status) {
|
||||
scope.credential_name = '';
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to ' + url + '. GET returned: ' + status });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (scope.removeGetCreatedByNames) {
|
||||
scope.removeGetCreatedByNames();
|
||||
}
|
||||
scope.removeGetCreatedByNames = scope.$on('GetCreatedByNames', function(e, data) {
|
||||
var url;
|
||||
data = data.slice(0, data.length-1);
|
||||
data = data.slice(data.lastIndexOf('/')+1, data.length);
|
||||
url = GetBasePath('users') + data + '/';
|
||||
scope.users_url = '/#/users/' + data;
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success( function(data) {
|
||||
scope.created_by = data.username;
|
||||
})
|
||||
.error( function(data, status) {
|
||||
scope.credential_name = '';
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to ' + url + '. GET returned: ' + status });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
if (scope.removeLoadJob) {
|
||||
scope.removeLoadJob();
|
||||
}
|
||||
@ -738,6 +705,24 @@ export function JobDetailController ($location, $rootScope, $filter, $scope, $co
|
||||
scope.verbosity = data.verbosity;
|
||||
scope.job_tags = data.job_tags;
|
||||
scope.variables = ParseVariableString(data.extra_vars);
|
||||
scope.users_url = (data.summary_fields.created_by) ? '/#/users/' + data.summary_fields.created_by.id : '';
|
||||
scope.created_by = (data.summary_fields.created_by) ? data.summary_fields.created_by.username : '';
|
||||
|
||||
if (data.summary_fields.credential) {
|
||||
scope.credential_name = data.summary_fields.credential.name;
|
||||
scope.credential_url = data.related.credential
|
||||
.replace('api/v1', '#');
|
||||
} else {
|
||||
scope.credential_name = "";
|
||||
}
|
||||
|
||||
if (data.summary_fields.cloud_credential) {
|
||||
scope.cloud_credential_name = data.summary_fields.cloud_credential.name;
|
||||
scope.cloud_credential_url = data.related.cloud_credential
|
||||
.replace('api/v1', '#');
|
||||
} else {
|
||||
scope.cloud_credential_name = "";
|
||||
}
|
||||
|
||||
for (i=0; i < verbosity_options.length; i++) {
|
||||
if (verbosity_options[i].value === data.verbosity) {
|
||||
@ -792,10 +777,6 @@ export function JobDetailController ($location, $rootScope, $filter, $scope, $co
|
||||
});
|
||||
//scope.setSearchAll('host');
|
||||
scope.$emit('LoadPlays', data.related.job_events);
|
||||
scope.$emit('GetCreatedByNames', data.related.created_by);
|
||||
if (!scope.credential_name) {
|
||||
scope.$emit('GetCredentialNames', data);
|
||||
}
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
@ -1415,5 +1396,5 @@ export function JobDetailController ($location, $rootScope, $filter, $scope, $co
|
||||
JobDetailController.$inject = [ '$location', '$rootScope', '$filter', '$scope', '$compile', '$routeParams', '$log', 'ClearScope', 'Breadcrumbs', 'LoadBreadCrumbs', 'GetBasePath',
|
||||
'Wait', 'Rest', 'ProcessErrors', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'DrawGraph', 'LoadHostSummary', 'ReloadHostSummaryList',
|
||||
'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'EventViewer', 'DeleteJob', 'PlaybookRun', 'HostEventsViewer', 'LoadPlays', 'LoadTasks',
|
||||
'LoadHosts', 'HostsEdit', 'ParseVariableString', 'GetChoices'
|
||||
'LoadHosts', 'HostsEdit', 'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels'
|
||||
];
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
export function ProjectsList ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, ProjectList, GenerateList, LoadBreadCrumbs,
|
||||
Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, SelectionInit, ProjectUpdate,
|
||||
Refresh, Wait, Stream, GetChoices, Empty, Find, LogViewer, GetProjectIcon, GetProjectToolTip) {
|
||||
Refresh, Wait, Stream, GetChoices, Empty, Find, LogViewer, GetProjectIcon, GetProjectToolTip, $filter) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
@ -309,29 +309,37 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $routeParams,
|
||||
});
|
||||
|
||||
$scope.cancelUpdate = function (id, name) {
|
||||
// Start the cancel process
|
||||
var i, project, found = false;
|
||||
for (i = 0; i < $scope.projects.length; i++) {
|
||||
if ($scope.projects[i].id === id) {
|
||||
project = $scope.projects[i];
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found && project.related.current_update) {
|
||||
Rest.setUrl(project.related.current_update);
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
$scope.$emit('Check_Cancel', data);
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to ' + project.related.current_update + ' failed. GET status: ' + status });
|
||||
});
|
||||
} else {
|
||||
Alert('Update Not Found', 'An SCM update does not appear to be running for project: ' + name + '. Click the <em>Refresh</em> ' +
|
||||
'button to view the latet status.', 'alert-info');
|
||||
}
|
||||
// // Start the cancel process
|
||||
// var i, project, found = false;
|
||||
// for (i = 0; i < $scope.projects.length; i++) {
|
||||
// if ($scope.projects[i].id === id) {
|
||||
// project = $scope.projects[i];
|
||||
// found = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
Rest.setUrl(GetBasePath("projects") + id);
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
if (data.related.current_update) {
|
||||
Rest.setUrl(data.related.current_update);
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
$scope.$emit('Check_Cancel', data);
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to ' + data.related.current_update + ' failed. GET status: ' + status });
|
||||
});
|
||||
} else {
|
||||
Alert('Update Not Found', 'An SCM update does not appear to be running for project: ' + $filter('sanitize')(name) + '. Click the <em>Refresh</em> ' +
|
||||
'button to view the latest status.', 'alert-info',undefined,undefined,undefined,undefined,true);
|
||||
}
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to get project failed. GET status: ' + status });
|
||||
})
|
||||
};
|
||||
|
||||
$scope.refresh = function () {
|
||||
@ -384,7 +392,7 @@ export function ProjectsList ($scope, $rootScope, $location, $log, $routeParams,
|
||||
ProjectsList.$inject = ['$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'ProjectList', 'generateList',
|
||||
'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'GetBasePath',
|
||||
'SelectionInit', 'ProjectUpdate', 'Refresh', 'Wait', 'Stream', 'GetChoices', 'Empty', 'Find',
|
||||
'LogViewer', 'GetProjectIcon', 'GetProjectToolTip'
|
||||
'LogViewer', 'GetProjectIcon', 'GetProjectToolTip', '$filter'
|
||||
];
|
||||
|
||||
|
||||
|
||||
@ -176,7 +176,7 @@ TeamsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$r
|
||||
|
||||
export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeParams, TeamForm, GenerateForm, Rest, Alert, ProcessErrors,
|
||||
LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, LookUpInit, Prompt, GetBasePath, CheckAccess,
|
||||
OrganizationList, Wait, Stream, permissionsChoices, permissionsLabel, permissionsSearchSelect) {
|
||||
OrganizationList, Wait, Stream, fieldChoices, fieldLabels, permissionsSearchSelect) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
@ -192,16 +192,17 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeP
|
||||
$scope.permission_search_select = [];
|
||||
|
||||
// return a promise from the options request with the permission type choices (including adhoc) as a param
|
||||
var permissionsChoice = permissionsChoices({
|
||||
var permissionsChoice = fieldChoices({
|
||||
scope: $scope,
|
||||
url: 'api/v1/' + base + '/' + id + '/permissions/'
|
||||
url: 'api/v1/' + base + '/' + id + '/permissions/',
|
||||
field: 'permission_type'
|
||||
});
|
||||
|
||||
// manipulate the choices from the options request to be set on
|
||||
// scope and be usable by the list form
|
||||
permissionsChoice.then(function (choices) {
|
||||
choices =
|
||||
permissionsLabel({
|
||||
fieldLabels({
|
||||
choices: choices
|
||||
});
|
||||
_.map(choices, function(n, key) {
|
||||
@ -209,8 +210,6 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeP
|
||||
});
|
||||
});
|
||||
|
||||
$scope.team_id = id;
|
||||
|
||||
// manipulate the choices from the options request to be usable
|
||||
// by the search option for permission_type, you can't inject the
|
||||
// list until this is done!
|
||||
@ -221,8 +220,11 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeP
|
||||
});
|
||||
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
|
||||
generator.reset();
|
||||
$scope.$emit('loadTeam');
|
||||
});
|
||||
|
||||
$scope.team_id = id;
|
||||
|
||||
$scope.PermissionAddAllowed = false;
|
||||
|
||||
// Retrieve each related set and any lookups
|
||||
@ -253,59 +255,65 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeP
|
||||
}
|
||||
});
|
||||
|
||||
// Retrieve detail record and prepopulate the form
|
||||
Wait('start');
|
||||
Rest.setUrl(defaultUrl + ':id/');
|
||||
Rest.get({
|
||||
params: {
|
||||
id: id
|
||||
}
|
||||
})
|
||||
.success(function (data) {
|
||||
var fld, related, set;
|
||||
$scope.team_name = data.name;
|
||||
for (fld in form.fields) {
|
||||
if (data[fld]) {
|
||||
$scope[fld] = data[fld];
|
||||
master[fld] = $scope[fld];
|
||||
}
|
||||
// Retrieve each related set and any lookups
|
||||
if ($scope.loadTeamRemove) {
|
||||
$scope.loadTeamRemove();
|
||||
}
|
||||
$scope.loadTeamRemove = $scope.$on('loadTeam', function () {
|
||||
// Retrieve detail record and prepopulate the form
|
||||
Wait('start');
|
||||
Rest.setUrl(defaultUrl + ':id/');
|
||||
Rest.get({
|
||||
params: {
|
||||
id: id
|
||||
}
|
||||
related = data.related;
|
||||
for (set in form.related) {
|
||||
if (related[set]) {
|
||||
relatedSets[set] = {
|
||||
url: related[set],
|
||||
iterator: form.related[set].iterator
|
||||
};
|
||||
}
|
||||
}
|
||||
// Initialize related search functions. Doing it here to make sure relatedSets object is populated.
|
||||
RelatedSearchInit({
|
||||
scope: $scope,
|
||||
form: form,
|
||||
relatedSets: relatedSets
|
||||
});
|
||||
RelatedPaginateInit({
|
||||
scope: $scope,
|
||||
relatedSets: relatedSets
|
||||
});
|
||||
|
||||
LookUpInit({
|
||||
scope: $scope,
|
||||
form: form,
|
||||
current_item: data.organization,
|
||||
list: OrganizationList,
|
||||
field: 'organization',
|
||||
input_type: 'radio'
|
||||
});
|
||||
|
||||
$scope.organization_url = data.related.organization;
|
||||
$scope.$emit('teamLoaded');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to retrieve team: ' + $routeParams.team_id +
|
||||
'. GET status: ' + status });
|
||||
});
|
||||
.success(function (data) {
|
||||
var fld, related, set;
|
||||
$scope.team_name = data.name;
|
||||
for (fld in form.fields) {
|
||||
if (data[fld]) {
|
||||
$scope[fld] = data[fld];
|
||||
master[fld] = $scope[fld];
|
||||
}
|
||||
}
|
||||
related = data.related;
|
||||
for (set in form.related) {
|
||||
if (related[set]) {
|
||||
relatedSets[set] = {
|
||||
url: related[set],
|
||||
iterator: form.related[set].iterator
|
||||
};
|
||||
}
|
||||
}
|
||||
// Initialize related search functions. Doing it here to make sure relatedSets object is populated.
|
||||
RelatedSearchInit({
|
||||
scope: $scope,
|
||||
form: form,
|
||||
relatedSets: relatedSets
|
||||
});
|
||||
RelatedPaginateInit({
|
||||
scope: $scope,
|
||||
relatedSets: relatedSets
|
||||
});
|
||||
|
||||
LookUpInit({
|
||||
scope: $scope,
|
||||
form: form,
|
||||
current_item: data.organization,
|
||||
list: OrganizationList,
|
||||
field: 'organization',
|
||||
input_type: 'radio'
|
||||
});
|
||||
|
||||
$scope.organization_url = data.related.organization;
|
||||
$scope.$emit('teamLoaded');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to retrieve team: ' + $routeParams.team_id +
|
||||
'. GET status: ' + status });
|
||||
});
|
||||
});
|
||||
|
||||
$scope.getPermissionText = function () {
|
||||
if (this.permission.permission_type !== "admin" && this.permission.run_ad_hoc_commands) {
|
||||
@ -431,5 +439,5 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeP
|
||||
|
||||
TeamsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'TeamForm',
|
||||
'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', 'RelatedPaginateInit',
|
||||
'ReturnToCaller', 'ClearScope', 'LookUpInit', 'Prompt', 'GetBasePath', 'CheckAccess', 'OrganizationList', 'Wait', 'Stream', 'permissionsChoices', 'permissionsLabel', 'permissionsSearchSelect'
|
||||
'ReturnToCaller', 'ClearScope', 'LookUpInit', 'Prompt', 'GetBasePath', 'CheckAccess', 'OrganizationList', 'Wait', 'Stream', 'fieldChoices', 'fieldLabels', 'permissionsSearchSelect'
|
||||
];
|
||||
|
||||
@ -208,7 +208,7 @@ UsersAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$r
|
||||
|
||||
export function UsersEdit($scope, $rootScope, $compile, $location, $log, $routeParams, UserForm, GenerateForm, Rest, Alert,
|
||||
ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, GetBasePath,
|
||||
Prompt, CheckAccess, ResetForm, Wait, Stream, permissionsChoices, permissionsLabel, permissionsSearchSelect) {
|
||||
Prompt, CheckAccess, ResetForm, Wait, Stream, fieldChoices, fieldLabels, permissionsSearchSelect) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
@ -224,16 +224,17 @@ export function UsersEdit($scope, $rootScope, $compile, $location, $log, $routeP
|
||||
$scope.permission_search_select = [];
|
||||
|
||||
// return a promise from the options request with the permission type choices (including adhoc) as a param
|
||||
var permissionsChoice = permissionsChoices({
|
||||
var permissionsChoice = fieldChoices({
|
||||
scope: $scope,
|
||||
url: 'api/v1/' + base + '/' + id + '/permissions/'
|
||||
url: 'api/v1/' + base + '/' + id + '/permissions/',
|
||||
field: 'permission_type'
|
||||
});
|
||||
|
||||
// manipulate the choices from the options request to be set on
|
||||
// scope and be usable by the list form
|
||||
permissionsChoice.then(function (choices) {
|
||||
choices =
|
||||
permissionsLabel({
|
||||
fieldLabels({
|
||||
choices: choices
|
||||
});
|
||||
_.map(choices, function(n, key) {
|
||||
@ -241,22 +242,23 @@ export function UsersEdit($scope, $rootScope, $compile, $location, $log, $routeP
|
||||
});
|
||||
});
|
||||
|
||||
// manipulate the choices from the options request to be usable
|
||||
// by the search option for permission_type, you can't inject the
|
||||
// list until this is done!
|
||||
permissionsChoice.then(function (choices) {
|
||||
form.related.permissions.fields.permission_type.searchOptions =
|
||||
permissionsSearchSelect({
|
||||
choices: choices
|
||||
});
|
||||
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
|
||||
generator.reset();
|
||||
$scope.$emit("loadForm");
|
||||
});
|
||||
|
||||
if ($scope.removeFormReady) {
|
||||
$scope.removeFormReady();
|
||||
}
|
||||
$scope.removeFormReady = $scope.$on('formReady', function () {
|
||||
// manipulate the choices from the options request to be usable
|
||||
// by the search option for permission_type, you can't inject the
|
||||
// list until this is done!
|
||||
permissionsChoice.then(function (choices) {
|
||||
form.related.permissions.fields.permission_type.searchOptions =
|
||||
permissionsSearchSelect({
|
||||
choices: choices
|
||||
});
|
||||
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
|
||||
generator.reset();
|
||||
});
|
||||
|
||||
if ($scope.removePostRefresh) {
|
||||
$scope.removePostRefresh();
|
||||
}
|
||||
@ -470,54 +472,60 @@ export function UsersEdit($scope, $rootScope, $compile, $location, $log, $routeP
|
||||
// Put form back to its original state
|
||||
ResetForm();
|
||||
|
||||
|
||||
if ($scope.removeModifyForm) {
|
||||
$scope.removeModifyForm();
|
||||
if ($scope.removeLoadForm) {
|
||||
$scope.removeLoadForm();
|
||||
}
|
||||
$scope.removeModifyForm = $scope.$on('modifyForm', function () {
|
||||
// Modify form based on LDAP settings
|
||||
Rest.setUrl(GetBasePath('config'));
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
var i, fld;
|
||||
if (data.user_ldap_fields) {
|
||||
for (i = 0; i < data.user_ldap_fields.length; i++) {
|
||||
fld = data.user_ldap_fields[i];
|
||||
if (form.fields[fld]) {
|
||||
form.fields[fld].readonly = true;
|
||||
form.fields[fld].editRequired = false;
|
||||
if (form.fields[fld].awRequiredWhen) {
|
||||
delete form.fields[fld].awRequiredWhen;
|
||||
$scope.removeLoadForm = $scope.$on('loadForm', function () {
|
||||
|
||||
|
||||
if ($scope.removeModifyForm) {
|
||||
$scope.removeModifyForm();
|
||||
}
|
||||
$scope.removeModifyForm = $scope.$on('modifyForm', function () {
|
||||
// Modify form based on LDAP settings
|
||||
Rest.setUrl(GetBasePath('config'));
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
var i, fld;
|
||||
if (data.user_ldap_fields) {
|
||||
for (i = 0; i < data.user_ldap_fields.length; i++) {
|
||||
fld = data.user_ldap_fields[i];
|
||||
if (form.fields[fld]) {
|
||||
form.fields[fld].readonly = true;
|
||||
form.fields[fld].editRequired = false;
|
||||
if (form.fields[fld].awRequiredWhen) {
|
||||
delete form.fields[fld].awRequiredWhen;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$scope.$emit('formReady');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to retrieve application config. GET status: ' + status });
|
||||
});
|
||||
});
|
||||
|
||||
Wait('start');
|
||||
Rest.setUrl(defaultUrl + id + '/');
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
if (data.ldap_dn !== null && data.ldap_dn !== undefined && data.ldap_dn !== '') {
|
||||
//this is an LDAP user
|
||||
$scope.$emit('modifyForm');
|
||||
} else {
|
||||
$scope.$emit('formReady');
|
||||
}
|
||||
$scope.$emit('formReady');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to retrieve application config. GET status: ' + status });
|
||||
msg: 'Failed to retrieve user: ' + id + '. GET status: ' + status });
|
||||
});
|
||||
});
|
||||
|
||||
Wait('start');
|
||||
Rest.setUrl(defaultUrl + id + '/');
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
if (data.ldap_dn !== null && data.ldap_dn !== undefined && data.ldap_dn !== '') {
|
||||
//this is an LDAP user
|
||||
$scope.$emit('modifyForm');
|
||||
} else {
|
||||
$scope.$emit('formReady');
|
||||
}
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to retrieve user: ' + id + '. GET status: ' + status });
|
||||
});
|
||||
}
|
||||
|
||||
UsersEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'UserForm', 'GenerateForm',
|
||||
'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', 'RelatedPaginateInit', 'ReturnToCaller', 'ClearScope',
|
||||
'GetBasePath', 'Prompt', 'CheckAccess', 'ResetForm', 'Wait', 'Stream', 'permissionsChoices', 'permissionsLabel', 'permissionsSearchSelect'
|
||||
'GetBasePath', 'Prompt', 'CheckAccess', 'ResetForm', 'Wait', 'Stream', 'fieldChoices', 'fieldLabels', 'permissionsSearchSelect'
|
||||
];
|
||||
|
||||
@ -118,8 +118,8 @@ export default
|
||||
sourceModel: 'inventory_script',
|
||||
sourceField: 'name',
|
||||
ngClick: 'lookUpInventory_script()' ,
|
||||
addRequired: false,
|
||||
editRequired: false,
|
||||
addRequired: true,
|
||||
editRequired: true,
|
||||
ngRequired: "source && source.value === 'custom'",
|
||||
},
|
||||
extra_vars: {
|
||||
|
||||
@ -75,7 +75,7 @@ export default
|
||||
scope: scope,
|
||||
// buttons: [],
|
||||
width: 710,
|
||||
height: 400,
|
||||
height: 450,
|
||||
minWidth: 300,
|
||||
resizable: false,
|
||||
callback: 'DialogReady',
|
||||
|
||||
@ -751,9 +751,9 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
|
||||
$compile(elem)(modal_scope);
|
||||
|
||||
var form_scope =
|
||||
generator.inject(GroupForm, { mode: 'edit', id: 'properties-tab', breadCrumbs: false, related: false, scope: properties_scope });
|
||||
generator.inject(GroupForm, { mode: mode, id: 'properties-tab', breadCrumbs: false, related: false, scope: properties_scope });
|
||||
var source_form_scope =
|
||||
generator.inject(SourceForm, { mode: 'edit', id: 'sources-tab', breadCrumbs: false, related: false, scope: sources_scope });
|
||||
generator.inject(SourceForm, { mode: mode, id: 'sources-tab', breadCrumbs: false, related: false, scope: sources_scope });
|
||||
|
||||
//generator.reset();
|
||||
|
||||
@ -1465,6 +1465,11 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name
|
||||
|
||||
// Change the lookup and regions when the source changes
|
||||
sources_scope.sourceChange = function () {
|
||||
sources_scope.credential_name = "";
|
||||
sources_scope.credential = "";
|
||||
if (sources_scope.credential_name_api_error) {
|
||||
delete sources_scope.credential_name_api_error;
|
||||
}
|
||||
parent_scope.showSchedulesTab = (mode === 'edit' && sources_scope.source && sources_scope.source.value!=="manual") ? true : false;
|
||||
SourceChange({ scope: sources_scope, form: SourceForm });
|
||||
};
|
||||
|
||||
@ -25,9 +25,11 @@ export default
|
||||
|
||||
.factory('CheckLicense', ['$rootScope', '$compile', 'CreateDialog', 'Store',
|
||||
'LicenseUpdateForm', 'GenerateForm', 'TextareaResize', 'ToJSON', 'GetBasePath',
|
||||
'Rest', 'ProcessErrors', 'Alert', 'IsAdmin', '$location',
|
||||
'Rest', 'ProcessErrors', 'Alert', 'IsAdmin', '$location', 'pendoService',
|
||||
'Authorization', 'Wait',
|
||||
function($rootScope, $compile, CreateDialog, Store, LicenseUpdateForm, GenerateForm,
|
||||
TextareaResize, ToJSON, GetBasePath, Rest, ProcessErrors, Alert, IsAdmin, $location) {
|
||||
TextareaResize, ToJSON, GetBasePath, Rest, ProcessErrors, Alert, IsAdmin, $location,
|
||||
pendoService, Authorization, Wait) {
|
||||
return {
|
||||
getRemainingDays: function(time_remaining) {
|
||||
// assumes time_remaining will be in seconds
|
||||
@ -164,21 +166,32 @@ export default
|
||||
if (typeof json_data === 'object' && Object.keys(json_data).length > 0) {
|
||||
Rest.setUrl(url);
|
||||
Rest.post(json_data)
|
||||
.success(function () {
|
||||
try {
|
||||
$('#license-modal-dialog').dialog('close');
|
||||
}
|
||||
catch(e) {
|
||||
// ignore
|
||||
}
|
||||
.success(function (response) {
|
||||
response.license_info = response;
|
||||
Alert('License Accepted', 'The Ansible Tower license was updated. To review or update the license, choose View License from the Setup menu.','alert-info');
|
||||
$rootScope.features = undefined;
|
||||
$location.path('/home');
|
||||
|
||||
Authorization.getLicense()
|
||||
.success(function (data) {
|
||||
Authorization.setLicense(data);
|
||||
pendoService.issuePendoIdentity();
|
||||
Wait("stop");
|
||||
$location.path('/home');
|
||||
})
|
||||
.error(function () {
|
||||
Wait('stop');
|
||||
Alert('Error', 'Failed to access license information. GET returned status: ' + status, 'alert-danger',
|
||||
$location.path('/logout'));
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
})
|
||||
.error(function (data, status) {
|
||||
.catch(function (response) {
|
||||
scope.license_json_api_error = "A valid license key in JSON format is required";
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to update license. POST returned: ' + status
|
||||
ProcessErrors(scope, response.data, response.status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to update license. POST returned: ' + response.status
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
||||
@ -39,16 +39,25 @@ angular.module('LoadConfigHelper', ['Utilities'])
|
||||
if(angular.isObject(response.data)){
|
||||
$AnsibleConfig = _.extend($AnsibleConfig, response.data);
|
||||
Store('AnsibleConfig', $AnsibleConfig);
|
||||
if ($rootScope.loginConfig) {
|
||||
$rootScope.loginConfig.resolve('config loaded');
|
||||
}
|
||||
$rootScope.$emit('ConfigReady');
|
||||
}
|
||||
else {
|
||||
$log.info('local_settings.json is not a valid object');
|
||||
if ($rootScope.loginConfig) {
|
||||
$rootScope.loginConfig.resolve('config loaded');
|
||||
}
|
||||
$rootScope.$emit('ConfigReady');
|
||||
}
|
||||
|
||||
}, function() {
|
||||
//local_settings.json not found
|
||||
$log.info('local_settings.json not found');
|
||||
if ($rootScope.loginConfig) {
|
||||
$rootScope.loginConfig.resolve('config loaded');
|
||||
}
|
||||
$rootScope.$emit('ConfigReady');
|
||||
});
|
||||
});
|
||||
@ -57,15 +66,16 @@ angular.module('LoadConfigHelper', ['Utilities'])
|
||||
// load config.js
|
||||
$log.info('attempting to load config.js');
|
||||
$http({ method:'GET', url: $basePath + 'config.js' })
|
||||
.success(function(data) {
|
||||
.then(function(response) {
|
||||
$log.info('loaded config.js');
|
||||
$AnsibleConfig = eval(data);
|
||||
$AnsibleConfig = eval(response.data);
|
||||
Store('AnsibleConfig', $AnsibleConfig);
|
||||
$rootScope.$emit('LoadConfig');
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to load ' + $basePath + '/config.js. GET status: ' + status
|
||||
.catch(function(response) {
|
||||
response.data = 'Failed to load ' + $basePath + '/config.js';
|
||||
ProcessErrors($rootScope, response, response.status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to load ' + $basePath + '/config.js.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -60,7 +60,8 @@ export default
|
||||
logout: function () {
|
||||
// the following puts our primary scope up for garbage collection, which
|
||||
// should prevent content flash from the prior user.
|
||||
var scope = angular.element(document.getElementById('main-view')).scope();
|
||||
|
||||
var x, scope = angular.element(document.getElementById('main-view')).scope();
|
||||
scope.$destroy();
|
||||
//$rootScope.$destroy();
|
||||
|
||||
@ -78,6 +79,9 @@ export default
|
||||
$cookieStore.remove('lastPath');
|
||||
$rootScope.lastPath = '/home';
|
||||
}
|
||||
x = Store('sessionTime');
|
||||
x[$rootScope.current_user.id].loggedIn = false;
|
||||
Store('sessionTime', x);
|
||||
|
||||
$rootScope.lastUser = $cookieStore.get('current_user').id;
|
||||
$cookieStore.remove('token_expires');
|
||||
@ -94,7 +98,7 @@ export default
|
||||
$rootScope.token_expires = null;
|
||||
$rootScope.login_username = null;
|
||||
$rootScope.login_password = null;
|
||||
$rootScope.sessionTimer.expireSession();
|
||||
$rootScope.sessionTimer.clearTimers();
|
||||
},
|
||||
|
||||
getLicense: function () {
|
||||
@ -112,6 +116,7 @@ export default
|
||||
var license = data.license_info;
|
||||
license.analytics_status = data.analytics_status;
|
||||
license.version = data.version;
|
||||
license.ansible_version = data.ansible_version;
|
||||
license.tested = false;
|
||||
Store('license', license);
|
||||
$rootScope.features = Store('license').features;
|
||||
@ -149,7 +154,6 @@ export default
|
||||
// store the response values in $rootScope so we can get to them later
|
||||
$rootScope.current_user = response.results[0];
|
||||
$cookieStore.put('current_user', response.results[0]); //keep in session cookie in the event of browser refresh
|
||||
$rootScope.$emit('OpenSocket');
|
||||
},
|
||||
|
||||
restoreUserInfo: function () {
|
||||
|
||||
@ -12,7 +12,8 @@ export default
|
||||
Store, $log) {
|
||||
return {
|
||||
setPendoOptions: function (config) {
|
||||
var options = {
|
||||
var tower_version = config.version.split('-')[0],
|
||||
options = {
|
||||
visitor: {
|
||||
id: null,
|
||||
role: null,
|
||||
@ -23,7 +24,9 @@ export default
|
||||
planLevel: config.license_type,
|
||||
planPrice: config.instance_count,
|
||||
creationDate: config.license_date,
|
||||
trial: config.trial
|
||||
trial: config.trial,
|
||||
tower_version: tower_version,
|
||||
ansible_version: config.ansible_version
|
||||
}
|
||||
};
|
||||
if(config.analytics_status === 'detailed'){
|
||||
|
||||
@ -23,22 +23,21 @@
|
||||
*/
|
||||
export default
|
||||
['$rootScope', '$cookieStore', 'transitionTo', 'CreateDialog', 'Authorization',
|
||||
'Store', '$interval',
|
||||
'Store', '$interval', '$location', '$q',
|
||||
function ($rootScope, $cookieStore, transitionTo, CreateDialog, Authorization,
|
||||
Store, $interval) {
|
||||
Store, $interval, $location, $q) {
|
||||
return {
|
||||
|
||||
sessionTime: null,
|
||||
timeout: null,
|
||||
|
||||
getSessionTime: function () {
|
||||
if(Store('sessionTime_'+$rootScope.current_user.id)){
|
||||
return Store('sessionTime_'+$rootScope.current_user.id);
|
||||
if(Store('sessionTime')){
|
||||
return Store('sessionTime')[$rootScope.current_user.id].time;
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
isExpired: function (increase) {
|
||||
@ -83,14 +82,24 @@ export default
|
||||
this.sessionTime = 0;
|
||||
this.clearTimers();
|
||||
$cookieStore.put('sessionExpired', true);
|
||||
transitionTo('signOut');
|
||||
},
|
||||
|
||||
moveForward: function () {
|
||||
var tm, t;
|
||||
var tm, t, x, y;
|
||||
tm = ($AnsibleConfig.session_timeout) ? $AnsibleConfig.session_timeout : 1800;
|
||||
t = new Date().getTime() + (tm * 1000);
|
||||
Store('sessionTime_'+$rootScope.current_user.id, t);
|
||||
x = {
|
||||
time: t,
|
||||
loggedIn: true
|
||||
};
|
||||
if(Store('sessionTime')){
|
||||
y = Store('sessionTime');
|
||||
}
|
||||
else {
|
||||
y = {};
|
||||
}
|
||||
y[$rootScope.current_user.id] = x;
|
||||
Store('sessionTime' , y);
|
||||
$rootScope.sessionExpired = false;
|
||||
$cookieStore.put('sessionExpired', false);
|
||||
this.startTimers();
|
||||
@ -148,6 +157,14 @@ export default
|
||||
$('#idle-modal').dialog('close');
|
||||
}
|
||||
that.expireSession('idle');
|
||||
$location.url('/login');
|
||||
}
|
||||
if(Store('sessionTime') &&
|
||||
Store('sessionTime')[$rootScope.current_user.id] &&
|
||||
Store('sessionTime')[$rootScope.current_user.id].loggedIn === false){
|
||||
that.expireSession();
|
||||
$location.url('/login');
|
||||
|
||||
}
|
||||
|
||||
}, 1000);
|
||||
@ -156,11 +173,15 @@ export default
|
||||
|
||||
clearTimers: function(){
|
||||
$interval.cancel($rootScope.expireTimer);
|
||||
delete $rootScope.expireTimer;
|
||||
},
|
||||
|
||||
init: function () {
|
||||
var deferred = $q.defer();
|
||||
this.moveForward();
|
||||
return this;
|
||||
deferred.resolve(this);
|
||||
return deferred.promise;
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -110,17 +110,11 @@ export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope', '$l
|
||||
setLoginFocus();
|
||||
});
|
||||
|
||||
|
||||
if ($AnsibleConfig.custom_logo) {
|
||||
scope.customLogo = "custom_console_logo.png"
|
||||
scope.customLogoPresent = true;
|
||||
} else {
|
||||
scope.customLogo = "login_modal_logo.png";
|
||||
scope.customLogoPresent = false;
|
||||
}
|
||||
|
||||
scope.customLoginInfo = $AnsibleConfig.custom_login_info;
|
||||
scope.customLoginInfoPresent = (scope.customLoginInfo) ? true : false;
|
||||
$rootScope.loginConfig.promise.then(function () {
|
||||
scope.customLogo = ($AnsibleConfig.custom_logo) ? "custom_console_logo.png" : "tower_console_logo.png";
|
||||
scope.customLoginInfo = $AnsibleConfig.custom_login_info;
|
||||
scope.customLoginInfoPresent = ($AnsibleConfig.customLoginInfo) ? true : false;
|
||||
});
|
||||
|
||||
// Reset the login form
|
||||
//scope.loginForm.login_username.$setPristine();
|
||||
@ -171,9 +165,12 @@ export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope', '$l
|
||||
Authorization.getUser()
|
||||
.success(function (data) {
|
||||
Authorization.setUserInfo(data);
|
||||
$rootScope.sessionTimer = Timer.init();
|
||||
$rootScope.user_is_superuser = data.results[0].is_superuser;
|
||||
scope.$emit('AuthorizationGetLicense');
|
||||
Timer.init().then(function(timer){
|
||||
$rootScope.sessionTimer = timer;
|
||||
$rootScope.$emit('OpenSocket');
|
||||
$rootScope.user_is_superuser = data.results[0].is_superuser;
|
||||
scope.$emit('AuthorizationGetLicense');
|
||||
});
|
||||
})
|
||||
.error(function (data, status) {
|
||||
Authorization.logout();
|
||||
@ -200,7 +197,7 @@ export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope', '$l
|
||||
function (data) {
|
||||
var key;
|
||||
Wait('stop');
|
||||
if (data.data.non_field_errors && data.data.non_field_errors.length === 0) {
|
||||
if (data && data.data && data.data.non_field_errors && data.data.non_field_errors.length === 0) {
|
||||
// show field specific errors returned by the API
|
||||
for (key in data.data) {
|
||||
scope[key + 'Error'] = data.data[key][0];
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
var options = [],
|
||||
error = "";
|
||||
|
||||
function parseGoogle(option, key) {
|
||||
function parseGoogle(option) {
|
||||
var newOption = {};
|
||||
|
||||
newOption.type = "google";
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
<div class="row">
|
||||
<div class="left-side col-sm-4 col-xs-12">
|
||||
<img id="about-modal-logo" src="static/assets/tower_console_bug.png">
|
||||
|
||||
</div>
|
||||
<div class="right-side col-sm-8 col-xs-12">
|
||||
<img id="about-modal-titlelogo" src="static/assets/tower_login_logo.png"><br>
|
||||
<p>Tower Version <span id='about-modal-version'></span></p>
|
||||
<textarea class="form-control" rows="2" readonly>Copyright 2015. All rights reserved.
Ansible and Ansible Tower are registered trademarks of Ansible, Inc.
</textarea>
|
||||
<br>
|
||||
<p>Visit <a href="http://www.ansible.com" target="_blank">Ansible.com</a> for more information!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -33,8 +33,24 @@
|
||||
<div class="form-horizontal" role="form" id="job-status-form">
|
||||
|
||||
<div class="form-group" ng-show="job_status.explanation">
|
||||
<label class="col-lg-2 col-md-12 col-sm-12 col-xs-12">Explanation</label>
|
||||
<div class="col-lg-10 col-md-12 col-sm-12 col-xs-12 job_status_explanation" ng-bind-html="job_status.explanation"></div>
|
||||
<label class="col-lg-2 col-md-2 col-sm-2 col-xs-3 col-xs-12">Explanation</label>
|
||||
<div class="col-lg-10 col-md-10 col-sm-10 col-xs-9 job_status_explanation"
|
||||
ng-show="!previousTaskFailed" ng-bind-html="job_status.explanation"></div>
|
||||
<div class="col-lg-10 col-md-10 col-sm-10 col-xs-9 job_status_explanation"
|
||||
ng-show="previousTaskFailed">Previous Task Failed
|
||||
<a
|
||||
href=""
|
||||
id="explanation_help"
|
||||
aw-pop-over="{{ task_detail }}"
|
||||
aw-pop-over-watch="task_detail"
|
||||
data-placement="bottom"
|
||||
data-container="body" class="help-link" over-title="Failure Detail"
|
||||
title=""
|
||||
tabindex="-1">
|
||||
<i class="fa fa-question-circle">
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="job_status.traceback">
|
||||
|
||||
@ -10,8 +10,8 @@
|
||||
* @description This controller for permissions add
|
||||
*/
|
||||
export default
|
||||
['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'permissionsForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ClearScope', 'GetBasePath', 'ReturnToCaller', 'InventoryList', 'ProjectList', 'LookUpInit', 'CheckAccess', 'Wait', 'permissionsCategoryChange', 'permissionsChoices', 'permissionsLabel',
|
||||
function($scope, $rootScope, $compile, $location, $log, $routeParams, permissionsForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ClearScope, GetBasePath, ReturnToCaller, InventoryList, ProjectList, LookUpInit, CheckAccess, Wait, permissionsCategoryChange, permissionsChoices, permissionsLabel) {
|
||||
['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'permissionsForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ClearScope', 'GetBasePath', 'ReturnToCaller', 'InventoryList', 'ProjectList', 'LookUpInit', 'CheckAccess', 'Wait', 'permissionsCategoryChange', 'fieldChoices', 'fieldLabels',
|
||||
function($scope, $rootScope, $compile, $location, $log, $routeParams, permissionsForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ClearScope, GetBasePath, ReturnToCaller, InventoryList, ProjectList, LookUpInit, CheckAccess, Wait, permissionsCategoryChange, fieldChoices, fieldLabels) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
@ -22,13 +22,14 @@ export default
|
||||
base = $location.path().replace(/^\//, '').split('/')[0],
|
||||
master = {};
|
||||
|
||||
var permissionsChoice = permissionsChoices({
|
||||
var permissionsChoice = fieldChoices({
|
||||
scope: $scope,
|
||||
url: 'api/v1/' + base + '/' + id + '/permissions/'
|
||||
url: 'api/v1/' + base + '/' + id + '/permissions/',
|
||||
field: 'permission_type'
|
||||
});
|
||||
|
||||
permissionsChoice.then(function (choices) {
|
||||
return permissionsLabel({
|
||||
return fieldLabels({
|
||||
choices: choices
|
||||
});
|
||||
}).then(function (choices) {
|
||||
|
||||
@ -10,8 +10,8 @@
|
||||
* @description This controller for permissions edit
|
||||
*/
|
||||
export default
|
||||
['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'permissionsForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'Prompt', 'GetBasePath', 'InventoryList', 'ProjectList', 'LookUpInit', 'CheckAccess', 'Wait', 'permissionsCategoryChange', 'permissionsChoices', 'permissionsLabel',
|
||||
function($scope, $rootScope, $compile, $location, $log, $routeParams, permissionsForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, Prompt, GetBasePath, InventoryList, ProjectList, LookUpInit, CheckAccess, Wait, permissionsCategoryChange, permissionsChoices, permissionsLabel) {
|
||||
['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'permissionsForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'Prompt', 'GetBasePath', 'InventoryList', 'ProjectList', 'LookUpInit', 'CheckAccess', 'Wait', 'permissionsCategoryChange', 'fieldChoices', 'fieldLabels',
|
||||
function($scope, $rootScope, $compile, $location, $log, $routeParams, permissionsForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, Prompt, GetBasePath, InventoryList, ProjectList, LookUpInit, CheckAccess, Wait, permissionsCategoryChange, fieldChoices, fieldLabels) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
@ -25,13 +25,14 @@ export default
|
||||
|
||||
$scope.permission_label = {};
|
||||
|
||||
var permissionsChoice = permissionsChoices({
|
||||
var permissionsChoice = fieldChoices({
|
||||
scope: $scope,
|
||||
url: 'api/v1/' + base + '/' + base_id + '/permissions/'
|
||||
url: 'api/v1/' + base + '/' + base_id + '/permissions/',
|
||||
field: 'permission_type'
|
||||
});
|
||||
|
||||
permissionsChoice.then(function (choices) {
|
||||
return permissionsLabel({
|
||||
return fieldLabels({
|
||||
choices: choices
|
||||
});
|
||||
}).then(function (choices) {
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
|
||||
|
||||
export default
|
||||
['$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'permissionsList', 'generateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'GetBasePath', 'CheckAccess', 'Wait', 'permissionsChoices', 'permissionsLabel', 'permissionsSearchSelect',
|
||||
function ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, permissionsList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, CheckAccess, Wait, permissionsChoices, permissionsLabel, permissionsSearchSelect) {
|
||||
['$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'permissionsList', 'generateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'GetBasePath', 'CheckAccess', 'Wait', 'fieldChoices', 'fieldLabels', 'permissionsSearchSelect',
|
||||
function ($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, permissionsList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, CheckAccess, Wait, fieldChoices, fieldLabels, permissionsSearchSelect) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
@ -27,16 +27,17 @@ export default
|
||||
$scope.permission_search_select = [];
|
||||
|
||||
// return a promise from the options request with the permission type choices (including adhoc) as a param
|
||||
var permissionsChoice = permissionsChoices({
|
||||
var permissionsChoice = fieldChoices({
|
||||
scope: $scope,
|
||||
url: 'api/v1/' + base + '/' + base_id + '/permissions/'
|
||||
url: 'api/v1/' + base + '/' + base_id + '/permissions/',
|
||||
field: 'permission_type'
|
||||
});
|
||||
|
||||
// manipulate the choices from the options request to be set on
|
||||
// scope and be usable by the list form
|
||||
permissionsChoice.then(function (choices) {
|
||||
choices =
|
||||
permissionsLabel({
|
||||
fieldLabels({
|
||||
choices: choices
|
||||
});
|
||||
_.map(choices, function(n, key) {
|
||||
|
||||
@ -12,8 +12,6 @@ import list from './shared/permissions.list';
|
||||
import form from './shared/permissions.form';
|
||||
|
||||
import permissionsCategoryChange from './shared/category-change.factory';
|
||||
import permissionsChoices from './shared/get-choices.factory';
|
||||
import permissionsLabel from './shared/get-labels.factory';
|
||||
import permissionsSearchSelect from './shared/get-search-select.factory';
|
||||
|
||||
export default
|
||||
@ -25,6 +23,4 @@ export default
|
||||
.factory('permissionsList', list)
|
||||
.factory('permissionsForm', form)
|
||||
.factory('permissionsCategoryChange', permissionsCategoryChange)
|
||||
.factory('permissionsChoices', permissionsChoices)
|
||||
.factory('permissionsLabel', permissionsLabel)
|
||||
.factory('permissionsSearchSelect', permissionsSearchSelect);
|
||||
|
||||
@ -16,24 +16,28 @@
|
||||
['Rest', 'ProcessErrors', function(Rest, ProcessErrors) {
|
||||
return function (params) {
|
||||
var scope = params.scope,
|
||||
url = params.url;
|
||||
url = params.url,
|
||||
field = params.field;
|
||||
|
||||
// Auto populate the field if there is only one result
|
||||
Rest.setUrl(url);
|
||||
return Rest.options()
|
||||
.then(function (data) {
|
||||
data = data.data;
|
||||
var choices = data.actions.GET.permission_type.choices;
|
||||
var choices = data.actions.GET[field].choices;
|
||||
|
||||
// manually add the adhoc label to the choices object
|
||||
choices.push(["adhoc",
|
||||
data.actions.GET.run_ad_hoc_commands.help_text]);
|
||||
// manually add the adhoc label to the choices object if
|
||||
// the permission_type field
|
||||
if (field === "permission_type") {
|
||||
choices.push(["adhoc",
|
||||
data.actions.GET.run_ad_hoc_commands.help_text]);
|
||||
}
|
||||
|
||||
return choices;
|
||||
})
|
||||
.catch(function (data, status) {
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to get permission type labels. Options requrest returned status: ' + status });
|
||||
msg: 'Failed to get ' + field + ' labels. Options requrest returned status: ' + status });
|
||||
});
|
||||
};
|
||||
}];
|
||||
@ -11,8 +11,8 @@
|
||||
*************************************************/
|
||||
|
||||
export default
|
||||
[ '$rootScope', '$q',
|
||||
function ($rootScope, $q) {
|
||||
[ '$rootScope', '$q', '$injector',
|
||||
function ($rootScope, $q, $injector) {
|
||||
return {
|
||||
response: function(config) {
|
||||
if(config.headers('auth-token-timeout') !== null){
|
||||
@ -21,8 +21,10 @@
|
||||
return config;
|
||||
},
|
||||
responseError: function(rejection){
|
||||
if( rejection.data && !_.isEmpty(rejection.data.detail) && rejection.data.detail === "Maximum per-user sessions reached"){
|
||||
if(rejection && rejection.data && rejection.data.detail && rejection.data.detail === "Maximum per-user sessions reached"){
|
||||
$rootScope.sessionTimer.expireSession('session_limit');
|
||||
var location = $injector.get('$location');
|
||||
location.url('/login');
|
||||
return $q.reject(rejection);
|
||||
}
|
||||
return $q.reject(rejection);
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
|
||||
import restServicesFactory from './restServices.factory';
|
||||
import interceptors from './interceptors.service';
|
||||
import fieldChoices from './get-choices.factory';
|
||||
import fieldLabels from './get-labels.factory';
|
||||
|
||||
export default
|
||||
angular.module('RestServices', [])
|
||||
@ -13,4 +15,6 @@ export default
|
||||
$httpProvider.interceptors.push('RestInterceptor');
|
||||
}])
|
||||
.factory('Rest', restServicesFactory)
|
||||
.service('RestInterceptor', interceptors);
|
||||
.service('RestInterceptor', interceptors)
|
||||
.factory('fieldChoices', fieldChoices)
|
||||
.factory('fieldLabels', fieldLabels);
|
||||
|
||||
@ -53,8 +53,8 @@
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<a link-to="managementJobsList" class="SetupItem SetupItem--aside HoverIcon Media">
|
||||
<i class="HoverIcon-icon HoverIcon-icon--opacity HoverIcon-icon--color Media-figure SetupItem-icon SetupItem-icon--aside" ng-if="user_is_superuser">
|
||||
<a link-to="managementJobsList" class="SetupItem SetupItem--aside HoverIcon Media" ng-if="user_is_superuser">
|
||||
<i class="HoverIcon-icon HoverIcon-icon--opacity HoverIcon-icon--color Media-figure SetupItem-icon SetupItem-icon--aside">
|
||||
<aw-icon name="ManagementJobs"></aw-icon>
|
||||
</i>
|
||||
<div class="Media-block">
|
||||
|
||||
@ -252,15 +252,20 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter'])
|
||||
if ((!fieldErrors) && defaultMsg) {
|
||||
Alert(defaultMsg.hdr, defaultMsg.msg);
|
||||
}
|
||||
} else if (typeof data === 'object' && Object.keys(data).length > 0) {
|
||||
keys = Object.keys(data);
|
||||
if (Array.isArray(data[keys[0]])) {
|
||||
msg = data[keys[0]][0];
|
||||
} else if (typeof data === 'object' && data !== null){
|
||||
if(Object.keys(data).length > 0) {
|
||||
keys = Object.keys(data);
|
||||
if (Array.isArray(data[keys[0]])) {
|
||||
msg = data[keys[0]][0];
|
||||
}
|
||||
else {
|
||||
msg = data[keys[0]];
|
||||
}
|
||||
Alert(defaultMsg.hdr, msg);
|
||||
}
|
||||
else {
|
||||
msg = data[keys[0]];
|
||||
Alert(defaultMsg.hdr, defaultMsg.msg);
|
||||
}
|
||||
Alert(defaultMsg.hdr, msg);
|
||||
} else {
|
||||
Alert(defaultMsg.hdr, defaultMsg.msg);
|
||||
}
|
||||
|
||||
@ -275,7 +275,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
|
||||
if (/^\-?\d*$/.test(viewValue)) {
|
||||
// it is valid
|
||||
ctrl.$setValidity('integer', true);
|
||||
if ( viewValue === '-' || viewValue === '-0' || viewValue === '' || viewValue === null) {
|
||||
if ( viewValue === '-' || viewValue === '-0' || viewValue === null) {
|
||||
ctrl.$setValidity('integer', false);
|
||||
return viewValue;
|
||||
}
|
||||
|
||||
@ -221,15 +221,68 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti
|
||||
if (obj2_obj && obj2_obj.name && !/^_delete/.test(obj2_obj.name)) {
|
||||
obj2_obj.base = obj2;
|
||||
obj2_obj.name = $filter('sanitize')(obj2_obj.name);
|
||||
descr += obj2 + " <a href=\"" + BuildUrl(obj2_obj) + "\">" + obj2_obj.name + '</a>' + ((activity.operation === 'disassociate') ? ' from ' : ' to ');
|
||||
descr_nolink += obj2 + ' ' + obj2_obj.name + ((activity.operation === 'disassociate') ? ' from ' : ' to ');
|
||||
descr += obj2 +
|
||||
" <a href=\"" + BuildUrl(obj2_obj) + "\">" +
|
||||
obj2_obj.name + '</a>';
|
||||
if (activity.object_association === 'admins') {
|
||||
if (activity.operation === 'disassociate') {
|
||||
descr += ' from being an admin of ';
|
||||
} else {
|
||||
descr += ' as an admin of ';
|
||||
}
|
||||
} else {
|
||||
if (activity.operation === 'disassociate') {
|
||||
descr += ' from ';
|
||||
} else {
|
||||
descr += ' to ';
|
||||
}
|
||||
}
|
||||
descr_nolink += obj2 + ' ' + obj2_obj.name;
|
||||
if (activity.object_association === 'admins') {
|
||||
if (activity.operation === 'disassociate') {
|
||||
descr_nolink += ' from being an admin of ';
|
||||
} else {
|
||||
descr_nolink += ' as an admin of ';
|
||||
}
|
||||
} else {
|
||||
if (activity.operation === 'disassociate') {
|
||||
descr_nolink += ' from ';
|
||||
} else {
|
||||
descr_nolink += ' to ';
|
||||
}
|
||||
}
|
||||
} else if (obj2) {
|
||||
name = '';
|
||||
if (obj2_obj && obj2_obj.name) {
|
||||
name = ' ' + stripDeleted(obj2_obj.name);
|
||||
}
|
||||
descr += obj2 + name + ((activity.operation === 'disassociate') ? ' from ' : ' to ');
|
||||
descr_nolink += obj2 + name + ((activity.operation === 'disassociate') ? ' from ' : ' to ');
|
||||
if (activity.object_association === 'admins') {
|
||||
if (activity.operation === 'disassociate') {
|
||||
descr += ' from being an admin of ';
|
||||
} else {
|
||||
descr += ' as an admin of ';
|
||||
}
|
||||
} else {
|
||||
if (activity.operation === 'disassociate') {
|
||||
descr += ' from ';
|
||||
} else {
|
||||
descr += ' to ';
|
||||
}
|
||||
}
|
||||
descr_nolink += obj2 + ' ' + obj2_obj.name;
|
||||
if (activity.object_association === 'admins') {
|
||||
if (activity.operation === 'disassociate') {
|
||||
descr_nolink += ' from being an admin of ';
|
||||
} else {
|
||||
descr_nolink += ' as an admin of ';
|
||||
}
|
||||
} else {
|
||||
if (activity.operation === 'disassociate') {
|
||||
descr_nolink += ' from ';
|
||||
} else {
|
||||
descr_nolink += ' to ';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (obj1_obj && obj1_obj.name && !/^\_delete/.test(obj1_obj.name)) {
|
||||
obj1_obj.base = obj1;
|
||||
|
||||
@ -31,7 +31,8 @@
|
||||
window.pendo_options = {
|
||||
|
||||
// This is required to be able to load data client side
|
||||
usePendoAgentAPI: true
|
||||
usePendoAgentAPI: true,
|
||||
disableGuides: true
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -55,7 +56,7 @@
|
||||
|
||||
<!-- Password Dialog -->
|
||||
<div id="password-modal" style="display: none;"></div>
|
||||
<div id="idle-modal" style="display:none">Your session will expire in <span id="remaining_seconds"></span> seconds, would you like to continue?</div>
|
||||
<div id="idle-modal" style="display:none">Your session will expire in <span id="remaining_seconds">60</span> seconds, would you like to continue?</div>
|
||||
|
||||
<!-- Generic Form dialog -->
|
||||
<div id="form-modal" class="modal fade">
|
||||
@ -156,7 +157,7 @@
|
||||
<div id="login-modal-dialog" style="display: none;"></div>
|
||||
<div id="help-modal-dialog" style="display: none;"></div>
|
||||
|
||||
<div id="about-modal-dialog" style="display: none;" ng-include=" '{{ STATIC_URL }}assets/cowsay-about.html ' "></div>
|
||||
<div class="About" id="about-modal-dialog" style="display: none;" ng-include=" '{{ STATIC_URL }}assets/cowsay-about.html ' "></div>
|
||||
|
||||
<div id="prompt-for-days" style="display:none">
|
||||
<form name="prompt_for_days_form" id="prompt_for_days_form">
|
||||
|
||||
@ -122,7 +122,7 @@ Jenkins
|
||||
|
||||
### Server Information ###
|
||||
|
||||
The AnsibleWorks Jenkins server can be found at http://50.116.42.103:8080/
|
||||
The Ansible Jenkins server can be found at http://jenkins.testing.ansible.com
|
||||
|
||||
This is a standard Jenkins installation, with the following additional
|
||||
plugins installed:
|
||||
|
||||
@ -38,16 +38,16 @@ successful release.
|
||||
|
||||
Monitor Jenkins
|
||||
---------------
|
||||
Once tagged, one must launch the [Release_Tower](http://50.116.42.103/view/Tower/job/Release_Tower/) with the following parameters:
|
||||
Once tagged, one must launch the [Release_Tower](http://jenkins.testing.ansible.com/view/Tower/job/Release_Tower/) with the following parameters:
|
||||
* `GIT_BRANCH=origin/tags/<X.Y.Z>`
|
||||
* `OFFICIAL=yes`
|
||||
|
||||
The following jobs will be triggered:
|
||||
* [Build_Tower_TAR](http://50.116.42.103/view/Tower/)
|
||||
* [Build_Tower_DEB](http://50.116.42.103/view/Tower/)
|
||||
* [Build_Tower_AMI](http://50.116.42.103/view/Tower/)
|
||||
* [Build_Tower_RPM](http://50.116.42.103/view/Tower/)
|
||||
* [Build_Tower_Docs](http://50.116.42.103/view/Tower/)
|
||||
* [Build_Tower_TAR](http://jenkins.testing.ansible.com/view/Tower/)
|
||||
* [Build_Tower_DEB](http://jenkins.testing.ansible.com/view/Tower/)
|
||||
* [Build_Tower_AMI](http://jenkins.testing.ansible.com/view/Tower/)
|
||||
* [Build_Tower_RPM](http://jenkins.testing.ansible.com/view/Tower/)
|
||||
* [Build_Tower_Docs](http://jenkins.testing.ansible.com/view/Tower/)
|
||||
|
||||
Should any build step fail, Jenkins will emit a message in IRC and set the build status to failed.
|
||||
|
||||
@ -66,4 +66,4 @@ While OFFICIAL Tower AMI's are created by jenkins, the process for blessing AMI'
|
||||
|
||||
Publishing Documentation
|
||||
------------------------
|
||||
Tower documentation is available in the [product-docs](https://github.com/ansible/product-docs) repository. The [Build_Tower_Docs](http://50.116.42.103/view/Tower/) job builds and publishes PDF, and HTML, documentation.
|
||||
Tower documentation is available in the [product-docs](https://github.com/ansible/product-docs) repository. The [Build_Tower_Docs](http://jenkins.testing.ansible.com/view/Tower/) job builds and publishes PDF, and HTML, documentation.
|
||||
|
||||
@ -20,11 +20,11 @@ django-celery==3.1.10
|
||||
django-crum==0.6.1
|
||||
django-extensions==1.3.3
|
||||
django-polymorphic==0.5.3
|
||||
django-radius==0.1.1
|
||||
django-radius==1.0.0
|
||||
djangorestframework==2.3.13
|
||||
django-split-settings==0.1.1
|
||||
django-taggit==0.11.2
|
||||
dm.xmlsec.binding==1.3.2
|
||||
git+https://github.com/matburt/dm.xmlsec.binding.git@master#egg=dm.xmlsec.binding
|
||||
dogpile.cache==0.5.6
|
||||
dogpile.core==0.4.1
|
||||
enum34==1.0.4
|
||||
@ -93,7 +93,7 @@ python-neutronclient==2.3.11
|
||||
python-novaclient==2.20.0
|
||||
python-openid==2.2.5
|
||||
python-radius==1.0
|
||||
python_social_auth==0.2.13
|
||||
git+https://github.com/matburt/python-social-auth.git@master#egg=python-social-auth
|
||||
python-saml==2.1.4
|
||||
python-swiftclient==2.2.0
|
||||
python-troveclient==1.0.9
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
-r requirements.txt
|
||||
ansible
|
||||
django-jenkins
|
||||
# Based on django-jenkins==0.16.3, with a fix for properly importing coverage
|
||||
git+https://github.com/jlaska/django-jenkins.git@release_0.16.4#egg=django-jenkins
|
||||
coverage
|
||||
pyflakes
|
||||
pep8
|
||||
|
||||
@ -18,4 +18,4 @@ exclude=.tox,awx/lib/site-packages,awx/plugins/inventory/ec2.py,awx/plugins/inve
|
||||
|
||||
[flake8]
|
||||
ignore=E201,E203,E221,E225,E231,E241,E251,E261,E265,E302,E303,E501,W291,W391,W293,E731
|
||||
exclude=.tox,awx/lib/site-packages,awx/plugins/inventory/ec2.py,awx/plugins/inventory/gce.py,awx/plugins/inventory/vmware.py,awx/plugins/inventory/windows_azure.py,awx/plugins/inventory/openstack.py,awx/plugins/inventory/rax.py,awx/ui,awx/api/urls.py,awx/main/migrations,awx/main/tests/data,node_modules/,awx/projects/,tools/docker
|
||||
exclude=.tox,awx/lib/site-packages,awx/plugins/inventory/ec2.py,awx/plugins/inventory/gce.py,awx/plugins/inventory/vmware.py,awx/plugins/inventory/windows_azure.py,awx/plugins/inventory/openstack.py,awx/plugins/inventory/rax.py,awx/ui,awx/api/urls.py,awx/main/migrations,awx/main/tests/data,node_modules/,awx/projects/,tools/docker,awx/settings/local_settings.py
|
||||
|
||||
2
setup.py
2
setup.py
@ -122,7 +122,7 @@ setup(
|
||||
("%s" % webconfig, ["config/awx-httpd-80.conf",
|
||||
"config/awx-httpd-443.conf",
|
||||
"config/awx-munin.conf"]),
|
||||
("%s" % sharedir, ["tools/scripts/request_tower_configuration.sh",]),
|
||||
("%s" % sharedir, ["tools/scripts/request_tower_configuration.sh","tools/scripts/request_tower_configuration.ps1"]),
|
||||
("%s" % docdir, ["docs/licenses/*",]),
|
||||
("%s" % munin_plugin_path, ["tools/munin_monitors/tower_jobs",
|
||||
"tools/munin_monitors/callbackr_alive",
|
||||
|
||||
2
tools/git_hooks/pre-commit
Executable file
2
tools/git_hooks/pre-commit
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
ansible-playbook -i "127.0.0.1," tools/git_hooks/pre_commit.yml
|
||||
8
tools/git_hooks/pre_commit.yml
Normal file
8
tools/git_hooks/pre_commit.yml
Normal file
@ -0,0 +1,8 @@
|
||||
- hosts: all
|
||||
connection: local
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: lint check w/ flake8
|
||||
command: 'flake8 {{ playbook_dir }}/../../'
|
||||
register: result
|
||||
|
||||
40
tools/scripts/request_tower_configuration.ps1
Normal file
40
tools/scripts/request_tower_configuration.ps1
Normal file
@ -0,0 +1,40 @@
|
||||
Param(
|
||||
[string]$tower_url,
|
||||
[string]$host_config_key,
|
||||
[string]$job_template_id
|
||||
)
|
||||
|
||||
Set-StrictMode -Version 2
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
If(-not $tower_url -or -not $host_config_key -or -not $job_template_id)
|
||||
{
|
||||
Write-Host "Requests server configuration from Ansible Tower"
|
||||
Write-Host "Usage: $($MyInvocation.MyCommand.Name) <server address>[:server port] <host config key> <job template id>"
|
||||
Write-Host "Example: $($MyInvocation.MyCommand.Name) example.towerhost.net 44d7507f2ead49af5fca80aa18fd24bc 38"
|
||||
Exit 1
|
||||
}
|
||||
|
||||
$retry_attempts = 10
|
||||
$attempt = 0
|
||||
|
||||
$data = @{
|
||||
host_config_key=$host_config_key
|
||||
}
|
||||
|
||||
While ($attempt -lt $retry_attempts) {
|
||||
Try {
|
||||
$resp = Invoke-WebRequest -Method POST -Body $data -Uri http://$tower_url/api/v1/job_templates/$job_template_id/callback/ -UseBasicParsing
|
||||
|
||||
If($resp.StatusCode -eq 202) {
|
||||
Exit 0
|
||||
}
|
||||
}
|
||||
Catch {
|
||||
$ex = $_
|
||||
$attempt++
|
||||
Write-Host "$([int]$ex.Exception.Response.StatusCode) received... retrying in 1 minute (Attempt $attempt)"
|
||||
}
|
||||
Start-Sleep -Seconds 60
|
||||
}
|
||||
Exit 1
|
||||
Loading…
x
Reference in New Issue
Block a user