mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Merge branch 'release_3.3.0' into devel
This commit is contained in:
commit
72f2994cc5
9
Makefile
9
Makefile
@ -296,7 +296,7 @@ uwsgi: collectstatic
|
||||
@if [ "$(VENV_BASE)" ]; then \
|
||||
. $(VENV_BASE)/awx/bin/activate; \
|
||||
fi; \
|
||||
uwsgi -b 32768 --socket 127.0.0.1:8050 --module=awx.wsgi:application --home=/venv/awx --chdir=/awx_devel/ --vacuum --processes=5 --harakiri=120 --master --no-orphans --py-autoreload 1 --max-requests=1000 --stats /tmp/stats.socket --master-fifo=/awxfifo --lazy-apps --logformat "%(addr) %(method) %(uri) - %(proto) %(status)" --hook-accepting1-once="exec:/bin/sh -c '[ -f /tmp/celery_pid ] && kill -1 `cat /tmp/celery_pid` || true'"
|
||||
uwsgi -b 32768 --socket 127.0.0.1:8050 --module=awx.wsgi:application --home=/venv/awx --chdir=/awx_devel/ --vacuum --processes=5 --harakiri=120 --master --no-orphans --py-autoreload 1 --max-requests=1000 --stats /tmp/stats.socket --lazy-apps --logformat "%(addr) %(method) %(uri) - %(proto) %(status)" --hook-accepting1-once="exec:/bin/sh -c '[ -f /tmp/celery_pid ] && kill -1 `cat /tmp/celery_pid` || true'"
|
||||
|
||||
daphne:
|
||||
@if [ "$(VENV_BASE)" ]; then \
|
||||
@ -372,13 +372,14 @@ awx-link:
|
||||
sed -i "s/placeholder/$(shell git describe --long | sed 's/\./\\./g')/" /awx_devel/awx.egg-info/PKG-INFO
|
||||
cp /tmp/awx.egg-link /venv/awx/lib/python2.7/site-packages/awx.egg-link
|
||||
|
||||
TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests
|
||||
TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests awx/network_ui/tests/unit
|
||||
|
||||
# Run all API unit tests.
|
||||
test:
|
||||
@if [ "$(VENV_BASE)" ]; then \
|
||||
. $(VENV_BASE)/awx/bin/activate; \
|
||||
fi; \
|
||||
py.test $(TEST_DIRS)
|
||||
py.test -n auto $(TEST_DIRS)
|
||||
|
||||
test_combined: test_ansible test
|
||||
|
||||
@ -386,7 +387,7 @@ test_unit:
|
||||
@if [ "$(VENV_BASE)" ]; then \
|
||||
. $(VENV_BASE)/awx/bin/activate; \
|
||||
fi; \
|
||||
py.test awx/main/tests/unit awx/conf/tests/unit awx/sso/tests/unit
|
||||
py.test awx/main/tests/unit awx/conf/tests/unit awx/sso/tests/unit awx/network_ui/tests/unit
|
||||
|
||||
test_ansible:
|
||||
@if [ "$(VENV_BASE)" ]; then \
|
||||
|
||||
@ -4,6 +4,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
# AWX
|
||||
from awx.conf import fields, register
|
||||
from awx.api.fields import OAuth2ProviderField
|
||||
from oauth2_provider.settings import oauth2_settings
|
||||
|
||||
|
||||
register(
|
||||
@ -36,7 +37,7 @@ register(
|
||||
register(
|
||||
'OAUTH2_PROVIDER',
|
||||
field_class=OAuth2ProviderField,
|
||||
default={'ACCESS_TOKEN_EXPIRE_SECONDS': 315360000000,
|
||||
default={'ACCESS_TOKEN_EXPIRE_SECONDS': oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||
'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600},
|
||||
label=_('OAuth 2 Timeout Settings'),
|
||||
help_text=_('Dictionary for customizing OAuth 2 timeouts, available items are '
|
||||
|
||||
@ -77,6 +77,63 @@ class TypeFilterBackend(BaseFilterBackend):
|
||||
raise ParseError(*e.args)
|
||||
|
||||
|
||||
def get_field_from_path(model, path):
|
||||
'''
|
||||
Given a Django ORM lookup path (possibly over multiple models)
|
||||
Returns the last field in the line, and also the revised lookup path
|
||||
ex., given
|
||||
model=Organization
|
||||
path='project__timeout'
|
||||
returns tuple of field at the end of the line as well as a corrected
|
||||
path, for special cases we do substitutions
|
||||
(<IntegerField for timeout>, 'project__timeout')
|
||||
'''
|
||||
# Store of all the fields used to detect repeats
|
||||
field_set = set([])
|
||||
new_parts = []
|
||||
for name in path.split('__'):
|
||||
if model is None:
|
||||
raise ParseError(_('No related model for field {}.').format(name))
|
||||
# HACK: Make project and inventory source filtering by old field names work for backwards compatibility.
|
||||
if model._meta.object_name in ('Project', 'InventorySource'):
|
||||
name = {
|
||||
'current_update': 'current_job',
|
||||
'last_update': 'last_job',
|
||||
'last_update_failed': 'last_job_failed',
|
||||
'last_updated': 'last_job_run',
|
||||
}.get(name, name)
|
||||
|
||||
if name == 'type' and 'polymorphic_ctype' in get_all_field_names(model):
|
||||
name = 'polymorphic_ctype'
|
||||
new_parts.append('polymorphic_ctype__model')
|
||||
else:
|
||||
new_parts.append(name)
|
||||
|
||||
if name in getattr(model, 'PASSWORD_FIELDS', ()):
|
||||
raise PermissionDenied(_('Filtering on password fields is not allowed.'))
|
||||
elif name == 'pk':
|
||||
field = model._meta.pk
|
||||
else:
|
||||
name_alt = name.replace("_", "")
|
||||
if name_alt in model._meta.fields_map.keys():
|
||||
field = model._meta.fields_map[name_alt]
|
||||
new_parts.pop()
|
||||
new_parts.append(name_alt)
|
||||
else:
|
||||
field = model._meta.get_field(name)
|
||||
if isinstance(field, ForeignObjectRel) and getattr(field.field, '__prevent_search__', False):
|
||||
raise PermissionDenied(_('Filtering on %s is not allowed.' % name))
|
||||
elif getattr(field, '__prevent_search__', False):
|
||||
raise PermissionDenied(_('Filtering on %s is not allowed.' % name))
|
||||
if field in field_set:
|
||||
# Field traversed twice, could create infinite JOINs, DoSing Tower
|
||||
raise ParseError(_('Loops not allowed in filters, detected on field {}.').format(field.name))
|
||||
field_set.add(field)
|
||||
model = getattr(field, 'related_model', None)
|
||||
|
||||
return field, '__'.join(new_parts)
|
||||
|
||||
|
||||
class FieldLookupBackend(BaseFilterBackend):
|
||||
'''
|
||||
Filter using field lookups provided via query string parameters.
|
||||
@ -91,61 +148,23 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
'isnull', 'search')
|
||||
|
||||
def get_field_from_lookup(self, model, lookup):
|
||||
field = None
|
||||
parts = lookup.split('__')
|
||||
if parts and parts[-1] not in self.SUPPORTED_LOOKUPS:
|
||||
parts.append('exact')
|
||||
|
||||
if '__' in lookup and lookup.rsplit('__', 1)[-1] in self.SUPPORTED_LOOKUPS:
|
||||
path, suffix = lookup.rsplit('__', 1)
|
||||
else:
|
||||
path = lookup
|
||||
suffix = 'exact'
|
||||
|
||||
if not path:
|
||||
raise ParseError(_('Query string field name not provided.'))
|
||||
|
||||
# FIXME: Could build up a list of models used across relationships, use
|
||||
# those lookups combined with request.user.get_queryset(Model) to make
|
||||
# sure user cannot query using objects he could not view.
|
||||
new_parts = []
|
||||
field, new_path = get_field_from_path(model, path)
|
||||
|
||||
# Store of all the fields used to detect repeats
|
||||
field_set = set([])
|
||||
|
||||
for name in parts[:-1]:
|
||||
# HACK: Make project and inventory source filtering by old field names work for backwards compatibility.
|
||||
if model._meta.object_name in ('Project', 'InventorySource'):
|
||||
name = {
|
||||
'current_update': 'current_job',
|
||||
'last_update': 'last_job',
|
||||
'last_update_failed': 'last_job_failed',
|
||||
'last_updated': 'last_job_run',
|
||||
}.get(name, name)
|
||||
|
||||
if name == 'type' and 'polymorphic_ctype' in get_all_field_names(model):
|
||||
name = 'polymorphic_ctype'
|
||||
new_parts.append('polymorphic_ctype__model')
|
||||
else:
|
||||
new_parts.append(name)
|
||||
|
||||
if name in getattr(model, 'PASSWORD_FIELDS', ()):
|
||||
raise PermissionDenied(_('Filtering on password fields is not allowed.'))
|
||||
elif name == 'pk':
|
||||
field = model._meta.pk
|
||||
else:
|
||||
name_alt = name.replace("_", "")
|
||||
if name_alt in model._meta.fields_map.keys():
|
||||
field = model._meta.fields_map[name_alt]
|
||||
new_parts.pop()
|
||||
new_parts.append(name_alt)
|
||||
else:
|
||||
field = model._meta.get_field(name)
|
||||
if 'auth' in name or 'token' in name:
|
||||
raise PermissionDenied(_('Filtering on %s is not allowed.' % name))
|
||||
if isinstance(field, ForeignObjectRel) and getattr(field.field, '__prevent_search__', False):
|
||||
raise PermissionDenied(_('Filtering on %s is not allowed.' % name))
|
||||
elif getattr(field, '__prevent_search__', False):
|
||||
raise PermissionDenied(_('Filtering on %s is not allowed.' % name))
|
||||
if field in field_set:
|
||||
# Field traversed twice, could create infinite JOINs, DoSing Tower
|
||||
raise ParseError(_('Loops not allowed in filters, detected on field {}.').format(field.name))
|
||||
field_set.add(field)
|
||||
model = getattr(field, 'related_model', None) or field.model
|
||||
|
||||
if parts:
|
||||
new_parts.append(parts[-1])
|
||||
new_lookup = '__'.join(new_parts)
|
||||
new_lookup = new_path
|
||||
new_lookup = '__'.join([new_path, suffix])
|
||||
return field, new_lookup
|
||||
|
||||
def to_python_related(self, value):
|
||||
@ -371,7 +390,7 @@ class OrderByBackend(BaseFilterBackend):
|
||||
else:
|
||||
order_by = (value,)
|
||||
if order_by:
|
||||
order_by = self._strip_sensitive_model_fields(queryset.model, order_by)
|
||||
order_by = self._validate_ordering_fields(queryset.model, order_by)
|
||||
|
||||
# Special handling of the type field for ordering. In this
|
||||
# case, we're not sorting exactly on the type field, but
|
||||
@ -396,15 +415,17 @@ class OrderByBackend(BaseFilterBackend):
|
||||
# Return a 400 for invalid field names.
|
||||
raise ParseError(*e.args)
|
||||
|
||||
def _strip_sensitive_model_fields(self, model, order_by):
|
||||
def _validate_ordering_fields(self, model, order_by):
|
||||
for field_name in order_by:
|
||||
# strip off the negation prefix `-` if it exists
|
||||
_field_name = field_name.split('-')[-1]
|
||||
prefix = ''
|
||||
path = field_name
|
||||
if field_name[0] == '-':
|
||||
prefix = field_name[0]
|
||||
path = field_name[1:]
|
||||
try:
|
||||
# if the field name is encrypted/sensitive, don't sort on it
|
||||
if _field_name in getattr(model, 'PASSWORD_FIELDS', ()) or \
|
||||
getattr(model._meta.get_field(_field_name), '__prevent_search__', False):
|
||||
raise ParseError(_('cannot order by field %s') % _field_name)
|
||||
except FieldDoesNotExist:
|
||||
pass
|
||||
yield field_name
|
||||
field, new_path = get_field_from_path(model, path)
|
||||
new_path = '{}{}'.format(prefix, new_path)
|
||||
except (FieldError, FieldDoesNotExist) as e:
|
||||
raise ParseError(e.args[0])
|
||||
yield new_path
|
||||
|
||||
@ -6,6 +6,7 @@ import inspect
|
||||
import logging
|
||||
import time
|
||||
import six
|
||||
import urllib
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
@ -29,6 +30,7 @@ from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework import views
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
|
||||
# cryptography
|
||||
from cryptography.fernet import InvalidToken
|
||||
@ -39,7 +41,7 @@ from awx.main.models import * # noqa
|
||||
from awx.main.access import access_registry
|
||||
from awx.main.utils import * # noqa
|
||||
from awx.main.utils.db import get_all_field_names
|
||||
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer
|
||||
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer
|
||||
from awx.api.versioning import URLPathVersioning, get_request_version
|
||||
from awx.api.metadata import SublistAttachDetatchMetadata, Metadata
|
||||
|
||||
@ -70,6 +72,13 @@ class LoggedLoginView(auth_views.LoginView):
|
||||
if current_user and getattr(current_user, 'pk', None) and current_user != original_user:
|
||||
logger.info("User {} logged in.".format(current_user.username))
|
||||
if request.user.is_authenticated:
|
||||
logger.info(smart_text(u"User {} logged in".format(self.request.user.username)))
|
||||
ret.set_cookie('userLoggedIn', 'true')
|
||||
current_user = UserSerializer(self.request.user)
|
||||
current_user = JSONRenderer().render(current_user.data)
|
||||
current_user = urllib.quote('%s' % current_user, '')
|
||||
ret.set_cookie('current_user', current_user)
|
||||
|
||||
return ret
|
||||
else:
|
||||
ret.status_code = 401
|
||||
@ -82,6 +91,7 @@ class LoggedLogoutView(auth_views.LogoutView):
|
||||
original_user = getattr(request, 'user', None)
|
||||
ret = super(LoggedLogoutView, self).dispatch(request, *args, **kwargs)
|
||||
current_user = getattr(request, 'user', None)
|
||||
ret.set_cookie('userLoggedIn', 'false')
|
||||
if (not current_user or not getattr(current_user, 'pk', True)) \
|
||||
and current_user != original_user:
|
||||
logger.info("User {} logged out.".format(original_user.username))
|
||||
@ -868,6 +878,9 @@ class CopyAPIView(GenericAPIView):
|
||||
obj, field.name, field_val
|
||||
)
|
||||
new_obj = model.objects.create(**create_kwargs)
|
||||
logger.debug(six.text_type('Deep copy: Created new object {}({})').format(
|
||||
new_obj, model
|
||||
))
|
||||
# Need to save separatedly because Djang-crum get_current_user would
|
||||
# not work properly in non-request-response-cycle context.
|
||||
new_obj.created_by = creater
|
||||
|
||||
@ -62,15 +62,11 @@ class Metadata(metadata.SimpleMetadata):
|
||||
opts = serializer.Meta.model._meta.concrete_model._meta
|
||||
verbose_name = smart_text(opts.verbose_name)
|
||||
field_info['help_text'] = field_help_text[field.field_name].format(verbose_name)
|
||||
# If field is not part of the model, then show it as non-filterable
|
||||
else:
|
||||
is_model_field = False
|
||||
for model_field in serializer.Meta.model._meta.fields:
|
||||
if field.field_name == model_field.name:
|
||||
is_model_field = True
|
||||
break
|
||||
if not is_model_field:
|
||||
field_info['filterable'] = False
|
||||
|
||||
for model_field in serializer.Meta.model._meta.fields:
|
||||
if field.field_name == model_field.name:
|
||||
field_info['filterable'] = True
|
||||
break
|
||||
|
||||
# Indicate if a field has a default value.
|
||||
# FIXME: Still isn't showing all default values?
|
||||
|
||||
@ -14,7 +14,6 @@ from datetime import timedelta
|
||||
|
||||
# OAuth2
|
||||
from oauthlib.common import generate_token
|
||||
from oauth2_provider.settings import oauth2_settings
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
@ -1024,7 +1023,7 @@ class UserAuthorizedTokenSerializer(BaseSerializer):
|
||||
validated_data['user'] = current_user
|
||||
validated_data['token'] = generate_token()
|
||||
validated_data['expires'] = now() + timedelta(
|
||||
seconds=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS
|
||||
seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS']
|
||||
)
|
||||
obj = super(OAuth2TokenSerializer, self).create(validated_data)
|
||||
obj.save()
|
||||
@ -1176,7 +1175,7 @@ class OAuth2TokenSerializer(BaseSerializer):
|
||||
validated_data['user'] = current_user
|
||||
validated_data['token'] = generate_token()
|
||||
validated_data['expires'] = now() + timedelta(
|
||||
seconds=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS
|
||||
seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS']
|
||||
)
|
||||
obj = super(OAuth2TokenSerializer, self).create(validated_data)
|
||||
if obj.application and obj.application.user:
|
||||
@ -1239,7 +1238,7 @@ class OAuth2AuthorizedTokenSerializer(BaseSerializer):
|
||||
validated_data['user'] = current_user
|
||||
validated_data['token'] = generate_token()
|
||||
validated_data['expires'] = now() + timedelta(
|
||||
seconds=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS
|
||||
seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS']
|
||||
)
|
||||
obj = super(OAuth2AuthorizedTokenSerializer, self).create(validated_data)
|
||||
if obj.application and obj.application.user:
|
||||
@ -1306,7 +1305,7 @@ class OAuth2PersonalTokenSerializer(BaseSerializer):
|
||||
validated_data['user'] = self.context['request'].user
|
||||
validated_data['token'] = generate_token()
|
||||
validated_data['expires'] = now() + timedelta(
|
||||
seconds=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS
|
||||
seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS']
|
||||
)
|
||||
validated_data['application'] = None
|
||||
obj = super(OAuth2PersonalTokenSerializer, self).create(validated_data)
|
||||
@ -4512,9 +4511,19 @@ class SchedulePreviewSerializer(BaseSerializer):
|
||||
class ScheduleSerializer(LaunchConfigurationBaseSerializer, SchedulePreviewSerializer):
|
||||
show_capabilities = ['edit', 'delete']
|
||||
|
||||
timezone = serializers.SerializerMethodField()
|
||||
until = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Schedule
|
||||
fields = ('*', 'unified_job_template', 'enabled', 'dtstart', 'dtend', 'rrule', 'next_run',)
|
||||
fields = ('*', 'unified_job_template', 'enabled', 'dtstart', 'dtend', 'rrule', 'next_run', 'timezone',
|
||||
'until')
|
||||
|
||||
def get_timezone(self, obj):
|
||||
return obj.timezone
|
||||
|
||||
def get_until(self, obj):
|
||||
return obj.until
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(ScheduleSerializer, self).get_related(obj)
|
||||
@ -4600,7 +4609,7 @@ class InstanceGroupSerializer(BaseSerializer):
|
||||
"this group when new instances come online.")
|
||||
)
|
||||
policy_instance_list = serializers.ListField(
|
||||
child=serializers.CharField(),
|
||||
child=serializers.CharField(), required=False,
|
||||
help_text=_("List of exact-match Instances that will be assigned to this group")
|
||||
)
|
||||
|
||||
@ -4627,6 +4636,11 @@ class InstanceGroupSerializer(BaseSerializer):
|
||||
raise serializers.ValidationError(_('{} is not a valid hostname of an existing instance.').format(instance_name))
|
||||
return value
|
||||
|
||||
def validate_name(self, value):
|
||||
if self.instance and self.instance.name == 'tower' and value != 'tower':
|
||||
raise serializers.ValidationError(_('tower instance group name may not be changed.'))
|
||||
return value
|
||||
|
||||
def get_jobs_qs(self):
|
||||
# Store running jobs queryset in context, so it will be shared in ListView
|
||||
if 'running_jobs' not in self.context:
|
||||
|
||||
@ -15,7 +15,7 @@ from awx.api.views import (
|
||||
UserActivityStreamList,
|
||||
UserAccessList,
|
||||
OAuth2ApplicationList,
|
||||
OAuth2TokenList,
|
||||
OAuth2UserTokenList,
|
||||
OAuth2PersonalTokenList,
|
||||
UserAuthorizedTokenList,
|
||||
)
|
||||
@ -32,7 +32,7 @@ urls = [
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', UserActivityStreamList.as_view(), name='user_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/access_list/$', UserAccessList.as_view(), name='user_access_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/tokens/$', OAuth2TokenList.as_view(), name='o_auth2_token_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/tokens/$', OAuth2UserTokenList.as_view(), name='o_auth2_token_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/authorized_tokens/$', UserAuthorizedTokenList.as_view(), name='user_authorized_token_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/personal_tokens/$', OAuth2PersonalTokenList.as_view(), name='o_auth2_personal_token_list'),
|
||||
|
||||
|
||||
@ -404,9 +404,11 @@ class ApiV1ConfigView(APIView):
|
||||
data.update(dict(
|
||||
project_base_dir = settings.PROJECTS_ROOT,
|
||||
project_local_paths = Project.get_local_path_choices(),
|
||||
custom_virtualenvs = get_custom_venv_choices(),
|
||||
))
|
||||
|
||||
if JobTemplate.accessible_objects(request.user, 'admin_role').exists():
|
||||
data['custom_virtualenvs'] = get_custom_venv_choices()
|
||||
|
||||
return Response(data)
|
||||
|
||||
def post(self, request):
|
||||
@ -610,6 +612,7 @@ class InstanceList(ListAPIView):
|
||||
view_name = _("Instances")
|
||||
model = Instance
|
||||
serializer_class = InstanceSerializer
|
||||
search_fields = ('hostname',)
|
||||
|
||||
|
||||
class InstanceDetail(RetrieveUpdateAPIView):
|
||||
@ -696,6 +699,7 @@ class InstanceGroupInstanceList(InstanceGroupMembershipMixin, SubListAttachDetac
|
||||
serializer_class = InstanceSerializer
|
||||
parent_model = InstanceGroup
|
||||
relationship = "instances"
|
||||
search_fields = ('hostname',)
|
||||
|
||||
|
||||
class ScheduleList(ListAPIView):
|
||||
@ -745,11 +749,11 @@ class ScheduleZoneInfo(APIView):
|
||||
swagger_topic = 'System Configuration'
|
||||
|
||||
def get(self, request):
|
||||
from dateutil.zoneinfo import get_zonefile_instance
|
||||
return Response([
|
||||
zones = [
|
||||
{'name': zone}
|
||||
for zone in sorted(get_zonefile_instance().zones)
|
||||
])
|
||||
for zone in Schedule.get_zoneinfo()
|
||||
]
|
||||
return Response(zones)
|
||||
|
||||
|
||||
class LaunchConfigCredentialsBase(SubListAttachDetachAPIView):
|
||||
@ -1072,6 +1076,7 @@ class OrganizationActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIV
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = Organization
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
class OrganizationNotificationTemplatesList(SubListCreateAttachDetachAPIView):
|
||||
@ -1126,6 +1131,7 @@ class OrganizationObjectRolesList(SubListAPIView):
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = Organization
|
||||
search_fields = ('role_field', 'content_type__model',)
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
@ -1165,6 +1171,7 @@ class TeamRolesList(SubListAttachDetachAPIView):
|
||||
metadata_class = RoleMetadata
|
||||
parent_model = Team
|
||||
relationship='member_role.children'
|
||||
search_fields = ('role_field', 'content_type__model',)
|
||||
|
||||
def get_queryset(self):
|
||||
team = get_object_or_404(Team, pk=self.kwargs['pk'])
|
||||
@ -1202,6 +1209,7 @@ class TeamObjectRolesList(SubListAPIView):
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = Team
|
||||
search_fields = ('role_field', 'content_type__model',)
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
@ -1233,6 +1241,7 @@ class TeamActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = Team
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
@ -1328,6 +1337,7 @@ class ProjectActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = Project
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
@ -1415,6 +1425,7 @@ class ProjectUpdateEventsList(SubListAPIView):
|
||||
parent_model = ProjectUpdate
|
||||
relationship = 'project_update_events'
|
||||
view_name = _('Project Update Events List')
|
||||
search_fields = ('stdout',)
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS
|
||||
@ -1428,6 +1439,7 @@ class SystemJobEventsList(SubListAPIView):
|
||||
parent_model = SystemJob
|
||||
relationship = 'system_job_events'
|
||||
view_name = _('System Job Events List')
|
||||
search_fields = ('stdout',)
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS
|
||||
@ -1441,6 +1453,7 @@ class InventoryUpdateEventsList(SubListAPIView):
|
||||
parent_model = InventoryUpdate
|
||||
relationship = 'inventory_update_events'
|
||||
view_name = _('Inventory Update Events List')
|
||||
search_fields = ('stdout',)
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS
|
||||
@ -1468,6 +1481,7 @@ class ProjectUpdateNotificationsList(SubListAPIView):
|
||||
serializer_class = NotificationSerializer
|
||||
parent_model = ProjectUpdate
|
||||
relationship = 'notifications'
|
||||
search_fields = ('subject', 'notification_type', 'body',)
|
||||
|
||||
|
||||
class ProjectUpdateScmInventoryUpdates(SubListCreateAPIView):
|
||||
@ -1491,6 +1505,7 @@ class ProjectObjectRolesList(SubListAPIView):
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = Project
|
||||
search_fields = ('role_field', 'content_type__model',)
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
@ -1573,6 +1588,7 @@ class OAuth2ApplicationActivityStreamList(ActivityStreamEnforcementMixin, SubLis
|
||||
parent_model = OAuth2Application
|
||||
relationship = 'activitystream_set'
|
||||
swagger_topic = 'Authentication'
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
class OAuth2TokenList(ListCreateAPIView):
|
||||
@ -1582,6 +1598,18 @@ class OAuth2TokenList(ListCreateAPIView):
|
||||
model = OAuth2AccessToken
|
||||
serializer_class = OAuth2TokenSerializer
|
||||
swagger_topic = 'Authentication'
|
||||
|
||||
|
||||
class OAuth2UserTokenList(SubListCreateAPIView):
|
||||
|
||||
view_name = _("OAuth2 User Tokens")
|
||||
|
||||
model = OAuth2AccessToken
|
||||
serializer_class = OAuth2TokenSerializer
|
||||
parent_model = User
|
||||
relationship = 'main_oauth2accesstoken'
|
||||
parent_key = 'user'
|
||||
swagger_topic = 'Authentication'
|
||||
|
||||
|
||||
class OAuth2AuthorizedTokenList(SubListCreateAPIView):
|
||||
@ -1657,6 +1685,7 @@ class OAuth2TokenActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIVi
|
||||
parent_model = OAuth2AccessToken
|
||||
relationship = 'activitystream_set'
|
||||
swagger_topic = 'Authentication'
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
class UserTeamsList(ListAPIView):
|
||||
@ -1680,6 +1709,7 @@ class UserRolesList(SubListAttachDetachAPIView):
|
||||
parent_model = User
|
||||
relationship='roles'
|
||||
permission_classes = (IsAuthenticated,)
|
||||
search_fields = ('role_field', 'content_type__model',)
|
||||
|
||||
def get_queryset(self):
|
||||
u = get_object_or_404(User, pk=self.kwargs['pk'])
|
||||
@ -1766,6 +1796,7 @@ class UserActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = User
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
@ -1852,6 +1883,7 @@ class CredentialTypeActivityStreamList(ActivityStreamEnforcementMixin, SubListAP
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = CredentialType
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
# remove in 3.3
|
||||
@ -1965,6 +1997,7 @@ class CredentialActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIVie
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = Credential
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
class CredentialAccessList(ResourceAccessList):
|
||||
@ -1978,6 +2011,7 @@ class CredentialObjectRolesList(SubListAPIView):
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = Credential
|
||||
search_fields = ('role_field', 'content_type__model',)
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
@ -2018,6 +2052,7 @@ class InventoryScriptObjectRolesList(SubListAPIView):
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = CustomInventoryScript
|
||||
search_fields = ('role_field', 'content_type__model',)
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
@ -2105,6 +2140,7 @@ class InventoryActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = Inventory
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
@ -2132,6 +2168,7 @@ class InventoryObjectRolesList(SubListAPIView):
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = Inventory
|
||||
search_fields = ('role_field', 'content_type__model',)
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
@ -2275,6 +2312,7 @@ class HostActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = Host
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
@ -2288,6 +2326,7 @@ class HostFactVersionsList(SystemTrackingEnforcementMixin, ParentMixin, ListAPIV
|
||||
model = Fact
|
||||
serializer_class = FactVersionSerializer
|
||||
parent_model = Host
|
||||
search_fields = ('facts',)
|
||||
|
||||
def get_queryset(self):
|
||||
from_spec = self.request.query_params.get('from', None)
|
||||
@ -2521,6 +2560,7 @@ class GroupActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = Group
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
@ -2752,6 +2792,7 @@ class InventorySourceActivityStreamList(ActivityStreamEnforcementMixin, SubListA
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = InventorySource
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
class InventorySourceNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
|
||||
@ -2891,6 +2932,7 @@ class InventoryUpdateNotificationsList(SubListAPIView):
|
||||
serializer_class = NotificationSerializer
|
||||
parent_model = InventoryUpdate
|
||||
relationship = 'notifications'
|
||||
search_fields = ('subject', 'notification_type', 'body',)
|
||||
|
||||
|
||||
class JobTemplateList(ListCreateAPIView):
|
||||
@ -3229,6 +3271,7 @@ class JobTemplateActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIVi
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = JobTemplate
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
class JobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
|
||||
@ -3512,6 +3555,7 @@ class JobTemplateObjectRolesList(SubListAPIView):
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = JobTemplate
|
||||
search_fields = ('role_field', 'content_type__model',)
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
@ -3529,6 +3573,7 @@ class WorkflowJobNodeList(WorkflowsEnforcementMixin, ListAPIView):
|
||||
|
||||
model = WorkflowJobNode
|
||||
serializer_class = WorkflowJobNodeListSerializer
|
||||
search_fields = ('unified_job_template__name', 'unified_job_template__description',)
|
||||
|
||||
|
||||
class WorkflowJobNodeDetail(WorkflowsEnforcementMixin, RetrieveAPIView):
|
||||
@ -3549,6 +3594,7 @@ class WorkflowJobTemplateNodeList(WorkflowsEnforcementMixin, ListCreateAPIView):
|
||||
|
||||
model = WorkflowJobTemplateNode
|
||||
serializer_class = WorkflowJobTemplateNodeSerializer
|
||||
search_fields = ('unified_job_template__name', 'unified_job_template__description',)
|
||||
|
||||
|
||||
class WorkflowJobTemplateNodeDetail(WorkflowsEnforcementMixin, RetrieveUpdateDestroyAPIView):
|
||||
@ -3570,6 +3616,7 @@ class WorkflowJobTemplateNodeChildrenBaseList(WorkflowsEnforcementMixin, Enforce
|
||||
parent_model = WorkflowJobTemplateNode
|
||||
relationship = ''
|
||||
enforce_parent_relationship = 'workflow_job_template'
|
||||
search_fields = ('unified_job_template__name', 'unified_job_template__description',)
|
||||
|
||||
'''
|
||||
Limit the set of WorkflowJobTemplateNodes to the related nodes of specified by
|
||||
@ -3639,6 +3686,7 @@ class WorkflowJobNodeChildrenBaseList(WorkflowsEnforcementMixin, SubListAPIView)
|
||||
serializer_class = WorkflowJobNodeListSerializer
|
||||
parent_model = WorkflowJobNode
|
||||
relationship = ''
|
||||
search_fields = ('unified_job_template__name', 'unified_job_template__description',)
|
||||
|
||||
#
|
||||
#Limit the set of WorkflowJobeNodes to the related nodes of specified by
|
||||
@ -3702,12 +3750,18 @@ class WorkflowJobTemplateCopy(WorkflowsEnforcementMixin, CopyAPIView):
|
||||
item = getattr(obj, field_name, None)
|
||||
if item is None:
|
||||
continue
|
||||
if field_name in ['inventory']:
|
||||
elif field_name in ['inventory']:
|
||||
if not user.can_access(item.__class__, 'use', item):
|
||||
setattr(obj, field_name, None)
|
||||
if field_name in ['unified_job_template']:
|
||||
elif field_name in ['unified_job_template']:
|
||||
if not user.can_access(item.__class__, 'start', item, validate_license=False):
|
||||
setattr(obj, field_name, None)
|
||||
elif field_name in ['credentials']:
|
||||
for cred in item.all():
|
||||
if not user.can_access(cred.__class__, 'use', cred):
|
||||
logger.debug(six.text_type(
|
||||
'Deep copy: removing {} from relationship due to permissions').format(cred))
|
||||
item.remove(cred.pk)
|
||||
obj.save()
|
||||
|
||||
|
||||
@ -3788,6 +3842,7 @@ class WorkflowJobTemplateWorkflowNodesList(WorkflowsEnforcementMixin, SubListCre
|
||||
parent_model = WorkflowJobTemplate
|
||||
relationship = 'workflow_job_template_nodes'
|
||||
parent_key = 'workflow_job_template'
|
||||
search_fields = ('unified_job_template__name', 'unified_job_template__description',)
|
||||
|
||||
def get_queryset(self):
|
||||
return super(WorkflowJobTemplateWorkflowNodesList, self).get_queryset().order_by('id')
|
||||
@ -3848,6 +3903,7 @@ class WorkflowJobTemplateObjectRolesList(WorkflowsEnforcementMixin, SubListAPIVi
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = WorkflowJobTemplate
|
||||
search_fields = ('role_field', 'content_type__model',)
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
@ -3861,6 +3917,7 @@ class WorkflowJobTemplateActivityStreamList(WorkflowsEnforcementMixin, ActivityS
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = WorkflowJobTemplate
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
@ -3890,6 +3947,7 @@ class WorkflowJobWorkflowNodesList(WorkflowsEnforcementMixin, SubListAPIView):
|
||||
parent_model = WorkflowJob
|
||||
relationship = 'workflow_job_nodes'
|
||||
parent_key = 'workflow_job'
|
||||
search_fields = ('unified_job_template__name', 'unified_job_template__description',)
|
||||
|
||||
def get_queryset(self):
|
||||
return super(WorkflowJobWorkflowNodesList, self).get_queryset().order_by('id')
|
||||
@ -3918,6 +3976,7 @@ class WorkflowJobNotificationsList(WorkflowsEnforcementMixin, SubListAPIView):
|
||||
serializer_class = NotificationSerializer
|
||||
parent_model = WorkflowJob
|
||||
relationship = 'notifications'
|
||||
search_fields = ('subject', 'notification_type', 'body',)
|
||||
|
||||
|
||||
class WorkflowJobActivityStreamList(WorkflowsEnforcementMixin, ActivityStreamEnforcementMixin, SubListAPIView):
|
||||
@ -3926,6 +3985,7 @@ class WorkflowJobActivityStreamList(WorkflowsEnforcementMixin, ActivityStreamEnf
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = WorkflowJob
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
class SystemJobTemplateList(ListAPIView):
|
||||
@ -4081,6 +4141,7 @@ class JobActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = Job
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
# TODO: remove endpoint in 3.3
|
||||
@ -4284,6 +4345,7 @@ class JobNotificationsList(SubListAPIView):
|
||||
serializer_class = NotificationSerializer
|
||||
parent_model = Job
|
||||
relationship = 'notifications'
|
||||
search_fields = ('subject', 'notification_type', 'body',)
|
||||
|
||||
|
||||
class BaseJobHostSummariesList(SubListAPIView):
|
||||
@ -4293,6 +4355,7 @@ class BaseJobHostSummariesList(SubListAPIView):
|
||||
parent_model = None # Subclasses must define this attribute.
|
||||
relationship = 'job_host_summaries'
|
||||
view_name = _('Job Host Summaries List')
|
||||
search_fields = ('host_name',)
|
||||
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
@ -4325,6 +4388,7 @@ class JobEventList(ListAPIView):
|
||||
|
||||
model = JobEvent
|
||||
serializer_class = JobEventSerializer
|
||||
search_fields = ('stdout',)
|
||||
|
||||
|
||||
class JobEventDetail(RetrieveAPIView):
|
||||
@ -4340,6 +4404,7 @@ class JobEventChildrenList(SubListAPIView):
|
||||
parent_model = JobEvent
|
||||
relationship = 'children'
|
||||
view_name = _('Job Event Children List')
|
||||
search_fields = ('stdout',)
|
||||
|
||||
|
||||
class JobEventHostsList(HostRelatedSearchMixin, SubListAPIView):
|
||||
@ -4553,6 +4618,7 @@ class AdHocCommandEventList(ListAPIView):
|
||||
|
||||
model = AdHocCommandEvent
|
||||
serializer_class = AdHocCommandEventSerializer
|
||||
search_fields = ('stdout',)
|
||||
|
||||
|
||||
class AdHocCommandEventDetail(RetrieveAPIView):
|
||||
@ -4568,6 +4634,7 @@ class BaseAdHocCommandEventsList(SubListAPIView):
|
||||
parent_model = None # Subclasses must define this attribute.
|
||||
relationship = 'ad_hoc_command_events'
|
||||
view_name = _('Ad Hoc Command Events List')
|
||||
search_fields = ('stdout',)
|
||||
|
||||
|
||||
class HostAdHocCommandEventsList(BaseAdHocCommandEventsList):
|
||||
@ -4590,6 +4657,7 @@ class AdHocCommandActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIV
|
||||
serializer_class = ActivityStreamSerializer
|
||||
parent_model = AdHocCommand
|
||||
relationship = 'activitystream_set'
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
class AdHocCommandNotificationsList(SubListAPIView):
|
||||
@ -4598,6 +4666,7 @@ class AdHocCommandNotificationsList(SubListAPIView):
|
||||
serializer_class = NotificationSerializer
|
||||
parent_model = AdHocCommand
|
||||
relationship = 'notifications'
|
||||
search_fields = ('subject', 'notification_type', 'body',)
|
||||
|
||||
|
||||
class SystemJobList(ListCreateAPIView):
|
||||
@ -4638,6 +4707,7 @@ class SystemJobNotificationsList(SubListAPIView):
|
||||
serializer_class = NotificationSerializer
|
||||
parent_model = SystemJob
|
||||
relationship = 'notifications'
|
||||
search_fields = ('subject', 'notification_type', 'body',)
|
||||
|
||||
|
||||
class UnifiedJobTemplateList(ListAPIView):
|
||||
@ -4706,7 +4776,6 @@ class UnifiedJobStdout(RetrieveAPIView):
|
||||
try:
|
||||
target_format = request.accepted_renderer.format
|
||||
if target_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)
|
||||
@ -4732,10 +4801,10 @@ class UnifiedJobStdout(RetrieveAPIView):
|
||||
if target_format == 'api':
|
||||
return Response(mark_safe(data))
|
||||
if target_format == 'json':
|
||||
if content_encoding == 'base64' and content_format == 'ansi':
|
||||
return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': b64encode(content.encode('utf-8'))})
|
||||
elif content_format == 'html':
|
||||
return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': body})
|
||||
content = content.encode('utf-8')
|
||||
if content_encoding == 'base64':
|
||||
content = b64encode(content)
|
||||
return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': content})
|
||||
return Response(data)
|
||||
elif target_format == 'txt':
|
||||
return Response(unified_job.result_stdout)
|
||||
@ -4843,6 +4912,7 @@ class NotificationTemplateNotificationList(SubListAPIView):
|
||||
parent_model = NotificationTemplate
|
||||
relationship = 'notifications'
|
||||
parent_key = 'notification_template'
|
||||
search_fields = ('subject', 'notification_type', 'body',)
|
||||
|
||||
|
||||
class NotificationTemplateCopy(CopyAPIView):
|
||||
@ -4855,6 +4925,7 @@ class NotificationList(ListAPIView):
|
||||
|
||||
model = Notification
|
||||
serializer_class = NotificationSerializer
|
||||
search_fields = ('subject', 'notification_type', 'body',)
|
||||
|
||||
|
||||
class NotificationDetail(RetrieveAPIView):
|
||||
@ -4879,6 +4950,7 @@ class ActivityStreamList(ActivityStreamEnforcementMixin, SimpleListAPIView):
|
||||
|
||||
model = ActivityStream
|
||||
serializer_class = ActivityStreamSerializer
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
class ActivityStreamDetail(ActivityStreamEnforcementMixin, RetrieveAPIView):
|
||||
@ -4892,6 +4964,7 @@ class RoleList(ListAPIView):
|
||||
model = Role
|
||||
serializer_class = RoleSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
search_fields = ('role_field', 'content_type__model',)
|
||||
|
||||
def get_queryset(self):
|
||||
result = Role.visible_roles(self.request.user)
|
||||
@ -5004,6 +5077,7 @@ class RoleParentsList(SubListAPIView):
|
||||
parent_model = Role
|
||||
relationship = 'parents'
|
||||
permission_classes = (IsAuthenticated,)
|
||||
search_fields = ('role_field', 'content_type__model',)
|
||||
|
||||
def get_queryset(self):
|
||||
role = Role.objects.get(pk=self.kwargs['pk'])
|
||||
@ -5017,6 +5091,7 @@ class RoleChildrenList(SubListAPIView):
|
||||
parent_model = Role
|
||||
relationship = 'children'
|
||||
permission_classes = (IsAuthenticated,)
|
||||
search_fields = ('role_field', 'content_type__model',)
|
||||
|
||||
def get_queryset(self):
|
||||
role = Role.objects.get(pk=self.kwargs['pk'])
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
from django.apps import AppConfig
|
||||
# from django.core import checks
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from awx.main.utils.handlers import configure_external_logger
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class ConfConfig(AppConfig):
|
||||
@ -11,16 +9,7 @@ class ConfConfig(AppConfig):
|
||||
name = 'awx.conf'
|
||||
verbose_name = _('Configuration')
|
||||
|
||||
def configure_oauth2_provider(self, settings):
|
||||
from oauth2_provider import settings as o_settings
|
||||
o_settings.oauth2_settings = o_settings.OAuth2ProviderSettings(
|
||||
settings.OAUTH2_PROVIDER, o_settings.DEFAULTS,
|
||||
o_settings.IMPORT_STRINGS, o_settings.MANDATORY
|
||||
)
|
||||
|
||||
def ready(self):
|
||||
self.module.autodiscover()
|
||||
from .settings import SettingsWrapper
|
||||
SettingsWrapper.initialize()
|
||||
configure_external_logger(settings)
|
||||
self.configure_oauth2_provider(settings)
|
||||
|
||||
@ -5,6 +5,8 @@ import logging
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import StringIO
|
||||
import traceback
|
||||
|
||||
import six
|
||||
|
||||
@ -62,11 +64,19 @@ __all__ = ['SettingsWrapper', 'get_settings_to_cache', 'SETTING_CACHE_NOTSET']
|
||||
def _log_database_error():
|
||||
try:
|
||||
yield
|
||||
except (ProgrammingError, OperationalError) as e:
|
||||
if get_tower_migration_version() < '310':
|
||||
except (ProgrammingError, OperationalError):
|
||||
if 'migrate' in sys.argv and get_tower_migration_version() < '310':
|
||||
logger.info('Using default settings until version 3.1 migration.')
|
||||
else:
|
||||
logger.warning('Database settings are not available, using defaults (%s)', e, exc_info=True)
|
||||
# Somewhat ugly - craming the full stack trace into the log message
|
||||
# the available exc_info does not give information about the real caller
|
||||
# TODO: replace in favor of stack_info kwarg in python 3
|
||||
sio = StringIO.StringIO()
|
||||
traceback.print_stack(file=sio)
|
||||
sinfo = sio.getvalue()
|
||||
sio.close()
|
||||
sinfo = sinfo.strip('\n')
|
||||
logger.warning('Database settings are not available, using defaults, logged from:\n{}'.format(sinfo))
|
||||
finally:
|
||||
pass
|
||||
|
||||
|
||||
0
awx/conf/tests/__init__.py
Normal file
0
awx/conf/tests/__init__.py
Normal file
@ -338,13 +338,14 @@ def test_setting_singleton_delete_no_read_only_fields(api_request, dummy_setting
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_setting_logging_test(api_request):
|
||||
with mock.patch('awx.conf.views.BaseHTTPSHandler.perform_test') as mock_func:
|
||||
with mock.patch('awx.conf.views.AWXProxyHandler.perform_test') as mock_func:
|
||||
api_request(
|
||||
'post',
|
||||
reverse('api:setting_logging_test'),
|
||||
data={'LOG_AGGREGATOR_HOST': 'http://foobar', 'LOG_AGGREGATOR_TYPE': 'logstash'}
|
||||
)
|
||||
test_arguments = mock_func.call_args[0][0]
|
||||
assert test_arguments.LOG_AGGREGATOR_HOST == 'http://foobar'
|
||||
assert test_arguments.LOG_AGGREGATOR_TYPE == 'logstash'
|
||||
assert test_arguments.LOG_AGGREGATOR_LEVEL == 'DEBUG'
|
||||
call = mock_func.call_args_list[0]
|
||||
args, kwargs = call
|
||||
given_settings = kwargs['custom_settings']
|
||||
assert given_settings.LOG_AGGREGATOR_HOST == 'http://foobar'
|
||||
assert given_settings.LOG_AGGREGATOR_TYPE == 'logstash'
|
||||
|
||||
6
awx/conf/tests/test_env.py
Normal file
6
awx/conf/tests/test_env.py
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
# Ensure that our autouse overwrites are working
|
||||
def test_cache(settings):
|
||||
assert settings.CACHES['default']['BACKEND'] == 'django.core.cache.backends.locmem.LocMemCache'
|
||||
assert settings.CACHES['default']['LOCATION'].startswith('unique-')
|
||||
@ -21,7 +21,7 @@ from awx.api.generics import * # noqa
|
||||
from awx.api.permissions import IsSuperUser
|
||||
from awx.api.versioning import reverse, get_request_version
|
||||
from awx.main.utils import * # noqa
|
||||
from awx.main.utils.handlers import BaseHTTPSHandler, UDPHandler, LoggingConnectivityException
|
||||
from awx.main.utils.handlers import AWXProxyHandler, LoggingConnectivityException
|
||||
from awx.main.tasks import handle_setting_changes
|
||||
from awx.conf.license import get_licensed_features
|
||||
from awx.conf.models import Setting
|
||||
@ -198,12 +198,9 @@ class SettingLoggingTest(GenericAPIView):
|
||||
mock_settings = MockSettings()
|
||||
for k, v in serializer.validated_data.items():
|
||||
setattr(mock_settings, k, v)
|
||||
mock_settings.LOG_AGGREGATOR_LEVEL = 'DEBUG'
|
||||
AWXProxyHandler().perform_test(custom_settings=mock_settings)
|
||||
if mock_settings.LOG_AGGREGATOR_PROTOCOL.upper() == 'UDP':
|
||||
UDPHandler.perform_test(mock_settings)
|
||||
return Response(status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
BaseHTTPSHandler.perform_test(mock_settings)
|
||||
except LoggingConnectivityException as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1366,6 +1366,7 @@ class JobTemplateAccess(BaseAccess):
|
||||
'job_tags', 'force_handlers', 'skip_tags', 'ask_variables_on_launch',
|
||||
'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_skip_tags_on_launch',
|
||||
'ask_inventory_on_launch', 'ask_credential_on_launch', 'survey_enabled',
|
||||
'custom_virtualenv', 'diff_mode',
|
||||
|
||||
# These fields are ignored, but it is convenient for QA to allow clients to post them
|
||||
'last_job_run', 'created', 'modified',
|
||||
@ -1814,13 +1815,14 @@ class WorkflowJobTemplateAccess(BaseAccess):
|
||||
missing_credentials = []
|
||||
missing_inventories = []
|
||||
qs = obj.workflow_job_template_nodes
|
||||
qs = qs.prefetch_related('unified_job_template', 'inventory__use_role', 'credential__use_role')
|
||||
qs = qs.prefetch_related('unified_job_template', 'inventory__use_role', 'credentials__use_role')
|
||||
for node in qs.all():
|
||||
node_errors = {}
|
||||
if node.inventory and self.user not in node.inventory.use_role:
|
||||
missing_inventories.append(node.inventory.name)
|
||||
if node.credential and self.user not in node.credential.use_role:
|
||||
missing_credentials.append(node.credential.name)
|
||||
for cred in node.credentials.all():
|
||||
if self.user not in cred.use_role:
|
||||
missing_credentials.append(cred.name)
|
||||
ujt = node.unified_job_template
|
||||
if ujt and not self.user.can_access(UnifiedJobTemplate, 'start', ujt, validate_license=False):
|
||||
missing_ujt.append(ujt.name)
|
||||
@ -1924,7 +1926,7 @@ class WorkflowJobAccess(BaseAccess):
|
||||
return self.can_recreate(obj)
|
||||
|
||||
def can_recreate(self, obj):
|
||||
node_qs = obj.workflow_job_nodes.all().prefetch_related('inventory', 'credential', 'unified_job_template')
|
||||
node_qs = obj.workflow_job_nodes.all().prefetch_related('inventory', 'credentials', 'unified_job_template')
|
||||
node_access = WorkflowJobNodeAccess(user=self.user)
|
||||
wj_add_perm = True
|
||||
for node in node_qs:
|
||||
|
||||
@ -193,8 +193,10 @@ def update_role_parentage_for_instance(instance):
|
||||
'''
|
||||
for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'):
|
||||
cur_role = getattr(instance, implicit_role_field.name)
|
||||
original_parents = set(json.loads(cur_role.implicit_parents))
|
||||
new_parents = implicit_role_field._resolve_parent_roles(instance)
|
||||
cur_role.parents.set(new_parents)
|
||||
cur_role.parents.remove(*list(original_parents - new_parents))
|
||||
cur_role.parents.add(*list(new_parents - original_parents))
|
||||
new_parents_list = list(new_parents)
|
||||
new_parents_list.sort()
|
||||
new_parents_json = json.dumps(new_parents_list)
|
||||
@ -802,23 +804,33 @@ class CredentialTypeInjectorField(JSONSchemaField):
|
||||
for field in model_instance.defined_fields
|
||||
)
|
||||
|
||||
class ExplodingNamespace:
|
||||
def __unicode__(self):
|
||||
raise UndefinedError(_('Must define unnamed file injector in order to reference `tower.filename`.'))
|
||||
|
||||
class TowerNamespace:
|
||||
filename = None
|
||||
def __init__(self):
|
||||
self.filename = ExplodingNamespace()
|
||||
|
||||
def __unicode__(self):
|
||||
raise UndefinedError(_('Cannot directly reference reserved `tower` namespace container.'))
|
||||
|
||||
valid_namespace['tower'] = TowerNamespace()
|
||||
|
||||
# ensure either single file or multi-file syntax is used (but not both)
|
||||
template_names = [x for x in value.get('file', {}).keys() if x.startswith('template')]
|
||||
if 'template' in template_names and len(template_names) > 1:
|
||||
raise django_exceptions.ValidationError(
|
||||
_('Must use multi-file syntax when injecting multiple files'),
|
||||
code='invalid',
|
||||
params={'value': value},
|
||||
)
|
||||
if 'template' not in template_names:
|
||||
valid_namespace['tower'].filename = TowerNamespace()
|
||||
if 'template' in template_names:
|
||||
valid_namespace['tower'].filename = 'EXAMPLE_FILENAME'
|
||||
if len(template_names) > 1:
|
||||
raise django_exceptions.ValidationError(
|
||||
_('Must use multi-file syntax when injecting multiple files'),
|
||||
code='invalid',
|
||||
params={'value': value},
|
||||
)
|
||||
elif template_names:
|
||||
for template_name in template_names:
|
||||
template_name = template_name.split('.')[1]
|
||||
setattr(valid_namespace['tower'].filename, template_name, 'EXAMPLE')
|
||||
setattr(valid_namespace['tower'].filename, template_name, 'EXAMPLE_FILENAME')
|
||||
|
||||
for type_, injector in value.items():
|
||||
for key, tmpl in injector.items():
|
||||
|
||||
@ -135,8 +135,7 @@ class AnsibleInventoryLoader(object):
|
||||
self.tmp_private_dir = build_proot_temp_dir()
|
||||
logger.debug("Using fresh temporary directory '{}' for isolation.".format(self.tmp_private_dir))
|
||||
kwargs['proot_temp_dir'] = self.tmp_private_dir
|
||||
# Run from source's location so that custom script contents are in `show_paths`
|
||||
cwd = functioning_dir(self.source)
|
||||
kwargs['proot_show_paths'] = [functioning_dir(self.source)]
|
||||
logger.debug("Running from `{}` working directory.".format(cwd))
|
||||
|
||||
return wrap_args_with_proot(cmd, cwd, **kwargs)
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
from awx.main.models import Instance, InstanceGroup
|
||||
from django.core.management.base import BaseCommand
|
||||
import six
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -13,10 +14,11 @@ class Command(BaseCommand):
|
||||
super(Command, self).__init__()
|
||||
|
||||
for instance in Instance.objects.all():
|
||||
print("hostname: {}; created: {}; heartbeat: {}; capacity: {}".format(instance.hostname, instance.created,
|
||||
instance.modified, instance.capacity))
|
||||
print(six.text_type(
|
||||
"hostname: {0.hostname}; created: {0.created}; "
|
||||
"heartbeat: {0.modified}; capacity: {0.capacity}").format(instance))
|
||||
for instance_group in InstanceGroup.objects.all():
|
||||
print("Instance Group: {}; created: {}; capacity: {}; members: {}".format(instance_group.name,
|
||||
instance_group.created,
|
||||
instance_group.capacity,
|
||||
[x.hostname for x in instance_group.instances.all()]))
|
||||
print(six.text_type(
|
||||
"Instance Group: {0.name}; created: {0.created}; "
|
||||
"capacity: {0.capacity}; members: {1}").format(instance_group,
|
||||
[x.hostname for x in instance_group.instances.all()]))
|
||||
|
||||
@ -19,11 +19,11 @@ class InstanceNotFound(Exception):
|
||||
class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--queuename', dest='queuename', type=str,
|
||||
parser.add_argument('--queuename', dest='queuename', type=lambda s: six.text_type(s, 'utf8'),
|
||||
help='Queue to create/update')
|
||||
parser.add_argument('--hostnames', dest='hostnames', type=str,
|
||||
parser.add_argument('--hostnames', dest='hostnames', type=lambda s: six.text_type(s, 'utf8'),
|
||||
help='Comma-Delimited Hosts to add to the Queue')
|
||||
parser.add_argument('--controller', dest='controller', type=str,
|
||||
parser.add_argument('--controller', dest='controller', type=lambda s: six.text_type(s, 'utf8'),
|
||||
default='', help='The controlling group (makes this an isolated group)')
|
||||
parser.add_argument('--instance_percent', dest='instance_percent', type=int, default=0,
|
||||
help='The percentage of active instances that will be assigned to this group'),
|
||||
@ -96,7 +96,7 @@ class Command(BaseCommand):
|
||||
if options.get('hostnames'):
|
||||
hostname_list = options.get('hostnames').split(",")
|
||||
|
||||
with advisory_lock('instance_group_registration_%s' % queuename):
|
||||
with advisory_lock(six.text_type('instance_group_registration_{}').format(queuename)):
|
||||
(ig, created, changed) = self.get_create_update_instance_group(queuename, inst_per, inst_min)
|
||||
if created:
|
||||
print(six.text_type("Creating instance group {}".format(ig.name)))
|
||||
|
||||
@ -95,7 +95,7 @@ class ReplayJobEvents():
|
||||
raise RuntimeError("Job is of type {} and replay is not yet supported.".format(type(job)))
|
||||
sys.exit(1)
|
||||
|
||||
def run(self, job_id, speed=1.0, verbosity=0):
|
||||
def run(self, job_id, speed=1.0, verbosity=0, skip=0):
|
||||
stats = {
|
||||
'events_ontime': {
|
||||
'total': 0,
|
||||
@ -126,7 +126,10 @@ class ReplayJobEvents():
|
||||
sys.exit(1)
|
||||
|
||||
je_previous = None
|
||||
for je_current in job_events:
|
||||
for n, je_current in enumerate(job_events):
|
||||
if n < skip:
|
||||
continue
|
||||
|
||||
if not je_previous:
|
||||
stats['recording_start'] = je_current.created
|
||||
self.start(je_current.created)
|
||||
@ -163,21 +166,25 @@ class ReplayJobEvents():
|
||||
|
||||
stats['events_total'] += 1
|
||||
je_previous = je_current
|
||||
|
||||
stats['replay_end'] = self.now()
|
||||
stats['replay_duration'] = (stats['replay_end'] - stats['replay_start']).total_seconds()
|
||||
stats['replay_start'] = stats['replay_start'].isoformat()
|
||||
stats['replay_end'] = stats['replay_end'].isoformat()
|
||||
|
||||
stats['recording_end'] = je_current.created
|
||||
stats['recording_duration'] = (stats['recording_end'] - stats['recording_start']).total_seconds()
|
||||
stats['recording_start'] = stats['recording_start'].isoformat()
|
||||
stats['recording_end'] = stats['recording_end'].isoformat()
|
||||
if stats['events_total'] > 2:
|
||||
stats['replay_end'] = self.now()
|
||||
stats['replay_duration'] = (stats['replay_end'] - stats['replay_start']).total_seconds()
|
||||
stats['replay_start'] = stats['replay_start'].isoformat()
|
||||
stats['replay_end'] = stats['replay_end'].isoformat()
|
||||
|
||||
stats['recording_end'] = je_current.created
|
||||
stats['recording_duration'] = (stats['recording_end'] - stats['recording_start']).total_seconds()
|
||||
stats['recording_start'] = stats['recording_start'].isoformat()
|
||||
stats['recording_end'] = stats['recording_end'].isoformat()
|
||||
|
||||
stats['events_ontime']['percentage'] = (stats['events_ontime']['total'] / float(stats['events_total'])) * 100.00
|
||||
stats['events_late']['percentage'] = (stats['events_late']['total'] / float(stats['events_total'])) * 100.00
|
||||
stats['events_distance_average'] = stats['events_distance_total'] / stats['events_total']
|
||||
stats['events_late']['lateness_average'] = stats['events_late']['lateness_total'] / stats['events_late']['total']
|
||||
else:
|
||||
stats = {'events_total': stats['events_total']}
|
||||
|
||||
stats['events_ontime']['percentage'] = (stats['events_ontime']['total'] / float(stats['events_total'])) * 100.00
|
||||
stats['events_late']['percentage'] = (stats['events_late']['total'] / float(stats['events_total'])) * 100.00
|
||||
stats['events_distance_average'] = stats['events_distance_total'] / stats['events_total']
|
||||
stats['events_late']['lateness_average'] = stats['events_late']['lateness_total'] / stats['events_late']['total']
|
||||
if verbosity >= 2:
|
||||
print(json.dumps(stats, indent=4, sort_keys=True))
|
||||
|
||||
@ -191,11 +198,14 @@ class Command(BaseCommand):
|
||||
help='Id of the job to replay (job or adhoc)')
|
||||
parser.add_argument('--speed', dest='speed', type=int, metavar='s',
|
||||
help='Speedup factor.')
|
||||
parser.add_argument('--skip', dest='skip', type=int, metavar='k',
|
||||
help='Number of events to skip.')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
job_id = options.get('job_id')
|
||||
speed = options.get('speed') or 1
|
||||
verbosity = options.get('verbosity') or 0
|
||||
skip = options.get('skip') or 0
|
||||
|
||||
replayer = ReplayJobEvents()
|
||||
replayer.run(job_id, speed, verbosity)
|
||||
replayer.run(job_id, speed, verbosity, skip)
|
||||
|
||||
@ -77,7 +77,7 @@ class InstanceManager(models.Manager):
|
||||
def me(self):
|
||||
"""Return the currently active instance."""
|
||||
# If we are running unit tests, return a stub record.
|
||||
if settings.IS_TESTING(sys.argv):
|
||||
if settings.IS_TESTING(sys.argv) or hasattr(sys, '_called_from_test'):
|
||||
return self.model(id=1,
|
||||
hostname='localhost',
|
||||
uuid='00000000-0000-0000-0000-000000000000')
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
# Django
|
||||
from django.conf import settings # noqa
|
||||
from django.db.models.signals import pre_delete # noqa
|
||||
|
||||
# AWX
|
||||
from awx.main.models.base import * # noqa
|
||||
@ -58,6 +59,18 @@ User.add_to_class('can_access_with_errors', check_user_access_with_errors)
|
||||
User.add_to_class('accessible_objects', user_accessible_objects)
|
||||
|
||||
|
||||
def cleanup_created_modified_by(sender, **kwargs):
|
||||
# work around a bug in django-polymorphic that doesn't properly
|
||||
# handle cascades for reverse foreign keys on the polymorphic base model
|
||||
# https://github.com/django-polymorphic/django-polymorphic/issues/229
|
||||
for cls in (UnifiedJobTemplate, UnifiedJob):
|
||||
cls.objects.filter(created_by=kwargs['instance']).update(created_by=None)
|
||||
cls.objects.filter(modified_by=kwargs['instance']).update(modified_by=None)
|
||||
|
||||
|
||||
pre_delete.connect(cleanup_created_modified_by, sender=User)
|
||||
|
||||
|
||||
@property
|
||||
def user_get_organizations(user):
|
||||
return Organization.objects.filter(member_role__members=user)
|
||||
@ -169,3 +182,9 @@ activity_stream_registrar.connect(OAuth2AccessToken)
|
||||
|
||||
# prevent API filtering on certain Django-supplied sensitive fields
|
||||
prevent_search(User._meta.get_field('password'))
|
||||
prevent_search(OAuth2AccessToken._meta.get_field('token'))
|
||||
prevent_search(RefreshToken._meta.get_field('token'))
|
||||
prevent_search(OAuth2Application._meta.get_field('client_secret'))
|
||||
prevent_search(OAuth2Application._meta.get_field('client_id'))
|
||||
prevent_search(Grant._meta.get_field('code'))
|
||||
|
||||
|
||||
@ -153,7 +153,7 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
|
||||
return reverse('api:ad_hoc_command_detail', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
def get_ui_url(self):
|
||||
return urljoin(settings.TOWER_URL_BASE, "/#/ad_hoc_commands/{}".format(self.pk))
|
||||
return urljoin(settings.TOWER_URL_BASE, "/#/jobs/command/{}".format(self.pk))
|
||||
|
||||
@property
|
||||
def notification_templates(self):
|
||||
|
||||
@ -4,9 +4,11 @@ import logging
|
||||
from django.conf import settings
|
||||
from django.db import models, DatabaseError
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from django.utils.text import Truncator
|
||||
from django.utils.timezone import utc
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.encoding import force_text
|
||||
import six
|
||||
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main.fields import JSONField
|
||||
@ -22,6 +24,22 @@ __all__ = ['JobEvent', 'ProjectUpdateEvent', 'AdHocCommandEvent',
|
||||
'InventoryUpdateEvent', 'SystemJobEvent']
|
||||
|
||||
|
||||
def sanitize_event_keys(kwargs, valid_keys):
|
||||
# Sanity check: Don't honor keys that we don't recognize.
|
||||
for key in kwargs.keys():
|
||||
if key not in valid_keys:
|
||||
kwargs.pop(key)
|
||||
|
||||
# Truncate certain values over 1k
|
||||
for key in [
|
||||
'play', 'role', 'task', 'playbook'
|
||||
]:
|
||||
if isinstance(kwargs.get(key), six.string_types):
|
||||
if len(kwargs[key]) > 1024:
|
||||
kwargs[key] = Truncator(kwargs[key]).chars(1024)
|
||||
|
||||
|
||||
|
||||
class BasePlaybookEvent(CreatedModifiedModel):
|
||||
'''
|
||||
An event/message logged from a playbook callback for each host.
|
||||
@ -257,7 +275,7 @@ class BasePlaybookEvent(CreatedModifiedModel):
|
||||
return updated_fields
|
||||
|
||||
@classmethod
|
||||
def create_from_data(self, **kwargs):
|
||||
def create_from_data(cls, **kwargs):
|
||||
pk = None
|
||||
for key in ('job_id', 'project_update_id'):
|
||||
if key in kwargs:
|
||||
@ -279,12 +297,8 @@ class BasePlaybookEvent(CreatedModifiedModel):
|
||||
except (KeyError, ValueError):
|
||||
kwargs.pop('created', None)
|
||||
|
||||
# Sanity check: Don't honor keys that we don't recognize.
|
||||
for key in kwargs.keys():
|
||||
if key not in self.VALID_KEYS:
|
||||
kwargs.pop(key)
|
||||
|
||||
job_event = self.objects.create(**kwargs)
|
||||
sanitize_event_keys(kwargs, cls.VALID_KEYS)
|
||||
job_event = cls.objects.create(**kwargs)
|
||||
analytics_logger.info('Event data saved.', extra=dict(python_objects=dict(job_event=job_event)))
|
||||
return job_event
|
||||
|
||||
@ -551,7 +565,7 @@ class BaseCommandEvent(CreatedModifiedModel):
|
||||
return u'%s @ %s' % (self.get_event_display(), self.created.isoformat())
|
||||
|
||||
@classmethod
|
||||
def create_from_data(self, **kwargs):
|
||||
def create_from_data(cls, **kwargs):
|
||||
# Convert the datetime for the event's creation
|
||||
# appropriately, and include a time zone for it.
|
||||
#
|
||||
@ -565,12 +579,8 @@ class BaseCommandEvent(CreatedModifiedModel):
|
||||
except (KeyError, ValueError):
|
||||
kwargs.pop('created', None)
|
||||
|
||||
# Sanity check: Don't honor keys that we don't recognize.
|
||||
for key in kwargs.keys():
|
||||
if key not in self.VALID_KEYS:
|
||||
kwargs.pop(key)
|
||||
|
||||
return self.objects.create(**kwargs)
|
||||
sanitize_event_keys(kwargs, cls.VALID_KEYS)
|
||||
return cls.objects.create(**kwargs)
|
||||
|
||||
def get_event_display(self):
|
||||
'''
|
||||
|
||||
@ -1646,7 +1646,7 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin,
|
||||
return reverse('api:inventory_update_detail', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
def get_ui_url(self):
|
||||
return urljoin(settings.TOWER_URL_BASE, "/#/inventory_sync/{}".format(self.pk))
|
||||
return urljoin(settings.TOWER_URL_BASE, "/#/jobs/inventory/{}".format(self.pk))
|
||||
|
||||
def get_actual_source_path(self):
|
||||
'''Alias to source_path that combines with project path for for SCM file based sources'''
|
||||
|
||||
@ -530,7 +530,7 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
|
||||
return reverse('api:job_detail', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
def get_ui_url(self):
|
||||
return urljoin(settings.TOWER_URL_BASE, "/#/jobs/{}".format(self.pk))
|
||||
return urljoin(settings.TOWER_URL_BASE, "/#/jobs/playbook/{}".format(self.pk))
|
||||
|
||||
@property
|
||||
def ansible_virtualenv_path(self):
|
||||
@ -1192,7 +1192,7 @@ class SystemJob(UnifiedJob, SystemJobOptions, JobNotificationMixin):
|
||||
return reverse('api:system_job_detail', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
def get_ui_url(self):
|
||||
return urljoin(settings.TOWER_URL_BASE, "/#/management_jobs/{}".format(self.pk))
|
||||
return urljoin(settings.TOWER_URL_BASE, "/#/jobs/system/{}".format(self.pk))
|
||||
|
||||
@property
|
||||
def event_class(self):
|
||||
|
||||
@ -241,6 +241,7 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
|
||||
SOFT_UNIQUE_TOGETHER = [('polymorphic_ctype', 'name', 'organization')]
|
||||
FIELDS_TO_PRESERVE_AT_COPY = ['labels', 'instance_groups', 'credentials']
|
||||
FIELDS_TO_DISCARD_AT_COPY = ['local_path']
|
||||
FIELDS_TRIGGER_UPDATE = frozenset(['scm_url', 'scm_branch', 'scm_type'])
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
@ -323,6 +324,11 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
|
||||
['name', 'description', 'schedule']
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
r = super(Project, self).__init__(*args, **kwargs)
|
||||
self._prior_values_store = self._current_sensitive_fields()
|
||||
return r
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
new_instance = not bool(self.pk)
|
||||
# If update_fields has been specified, add our field names to it,
|
||||
@ -354,9 +360,22 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
|
||||
with disable_activity_stream():
|
||||
self.save(update_fields=update_fields)
|
||||
# If we just created a new project with SCM, start the initial update.
|
||||
if new_instance and self.scm_type and not skip_update:
|
||||
# also update if certain fields have changed
|
||||
relevant_change = False
|
||||
new_values = self._current_sensitive_fields()
|
||||
if hasattr(self, '_prior_values_store') and self._prior_values_store != new_values:
|
||||
relevant_change = True
|
||||
self._prior_values_store = new_values
|
||||
if (relevant_change or new_instance) and (not skip_update) and self.scm_type:
|
||||
self.update()
|
||||
|
||||
def _current_sensitive_fields(self):
|
||||
new_values = {}
|
||||
for attr, val in self.__dict__.items():
|
||||
if attr in Project.FIELDS_TRIGGER_UPDATE:
|
||||
new_values[attr] = val
|
||||
return new_values
|
||||
|
||||
def _get_current_status(self):
|
||||
if self.scm_type:
|
||||
if self.current_job and self.current_job.status:
|
||||
@ -533,7 +552,7 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin, TaskManage
|
||||
return reverse('api:project_update_detail', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
def get_ui_url(self):
|
||||
return urlparse.urljoin(settings.TOWER_URL_BASE, "/#/scm_update/{}".format(self.pk))
|
||||
return urlparse.urljoin(settings.TOWER_URL_BASE, "/#/jobs/project/{}".format(self.pk))
|
||||
|
||||
def _update_parent_instance(self):
|
||||
parent_instance = self._get_parent_instance()
|
||||
|
||||
@ -172,7 +172,7 @@ class Role(models.Model):
|
||||
elif accessor.__class__.__name__ == 'Team':
|
||||
return self.ancestors.filter(pk=accessor.member_role.id).exists()
|
||||
elif type(accessor) == Role:
|
||||
return self.ancestors.filter(pk=accessor).exists()
|
||||
return self.ancestors.filter(pk=accessor.pk).exists()
|
||||
else:
|
||||
accessor_type = ContentType.objects.get_for_model(accessor)
|
||||
roles = Role.objects.filter(content_type__pk=accessor_type.id,
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import logging
|
||||
import datetime
|
||||
import logging
|
||||
import re
|
||||
|
||||
import dateutil.rrule
|
||||
from dateutil.tz import datetime_exists
|
||||
import dateutil.parser
|
||||
from dateutil.tz import datetime_exists, tzutc
|
||||
from dateutil.zoneinfo import get_zonefile_instance
|
||||
|
||||
# Django
|
||||
from django.db import models
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils.timezone import now
|
||||
from django.utils.timezone import now, make_aware
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# AWX
|
||||
@ -27,6 +31,9 @@ logger = logging.getLogger('awx.main.models.schedule')
|
||||
__all__ = ['Schedule']
|
||||
|
||||
|
||||
UTC_TIMEZONES = {x: tzutc() for x in dateutil.parser.parserinfo().UTCZONE}
|
||||
|
||||
|
||||
class ScheduleFilterMethods(object):
|
||||
|
||||
def enabled(self, enabled=True):
|
||||
@ -94,13 +101,98 @@ class Schedule(CommonModel, LaunchTimeConfig):
|
||||
help_text=_("The next time that the scheduled action will run.")
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_zoneinfo(self):
|
||||
return sorted(get_zonefile_instance().zones)
|
||||
|
||||
@property
|
||||
def timezone(self):
|
||||
utc = tzutc()
|
||||
all_zones = Schedule.get_zoneinfo()
|
||||
all_zones.sort(key = lambda x: -len(x))
|
||||
for r in Schedule.rrulestr(self.rrule)._rrule:
|
||||
if r._dtstart:
|
||||
tzinfo = r._dtstart.tzinfo
|
||||
if tzinfo is utc:
|
||||
return 'UTC'
|
||||
fname = tzinfo._filename
|
||||
for zone in all_zones:
|
||||
if fname.endswith(zone):
|
||||
return zone
|
||||
logger.warn('Could not detect valid zoneinfo for {}'.format(self.rrule))
|
||||
return ''
|
||||
|
||||
@property
|
||||
def until(self):
|
||||
# The UNTIL= datestamp (if any) coerced from UTC to the local naive time
|
||||
# of the DTSTART
|
||||
for r in Schedule.rrulestr(self.rrule)._rrule:
|
||||
if r._until:
|
||||
local_until = r._until.astimezone(r._dtstart.tzinfo)
|
||||
naive_until = local_until.replace(tzinfo=None)
|
||||
return naive_until.isoformat()
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def coerce_naive_until(cls, rrule):
|
||||
#
|
||||
# RFC5545 specifies that the UNTIL rule part MUST ALWAYS be a date
|
||||
# with UTC time. This is extra work for API implementers because
|
||||
# it requires them to perform DTSTART local -> UTC datetime coercion on
|
||||
# POST and UTC -> DTSTART local coercion on GET.
|
||||
#
|
||||
# This block of code is a departure from the RFC. If you send an
|
||||
# rrule like this to the API (without a Z on the UNTIL):
|
||||
#
|
||||
# DTSTART;TZID=America/New_York:20180502T150000 RRULE:FREQ=HOURLY;INTERVAL=1;UNTIL=20180502T180000
|
||||
#
|
||||
# ...we'll assume that the naive UNTIL is intended to match the DTSTART
|
||||
# timezone (America/New_York), and so we'll coerce to UTC _for you_
|
||||
# automatically.
|
||||
#
|
||||
if 'until=' in rrule.lower():
|
||||
# if DTSTART;TZID= is used, coerce "naive" UNTIL values
|
||||
# to the proper UTC date
|
||||
match_until = re.match(".*?(?P<until>UNTIL\=[0-9]+T[0-9]+)(?P<utcflag>Z?)", rrule)
|
||||
if not len(match_until.group('utcflag')):
|
||||
# rrule = DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000
|
||||
|
||||
# Find the UNTIL=N part of the string
|
||||
# naive_until = UNTIL=20200601T170000
|
||||
naive_until = match_until.group('until')
|
||||
|
||||
# What is the DTSTART timezone for:
|
||||
# DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000Z
|
||||
# local_tz = tzfile('/usr/share/zoneinfo/America/New_York')
|
||||
local_tz = dateutil.rrule.rrulestr(
|
||||
rrule.replace(naive_until, naive_until + 'Z'),
|
||||
tzinfos=UTC_TIMEZONES
|
||||
)._dtstart.tzinfo
|
||||
|
||||
# Make a datetime object with tzinfo=<the DTSTART timezone>
|
||||
# localized_until = datetime.datetime(2020, 6, 1, 17, 0, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York'))
|
||||
localized_until = make_aware(
|
||||
datetime.datetime.strptime(re.sub('^UNTIL=', '', naive_until), "%Y%m%dT%H%M%S"),
|
||||
local_tz
|
||||
)
|
||||
|
||||
# Coerce the datetime to UTC and format it as a string w/ Zulu format
|
||||
# utc_until = UNTIL=20200601T220000Z
|
||||
utc_until = 'UNTIL=' + localized_until.astimezone(pytz.utc).strftime('%Y%m%dT%H%M%SZ')
|
||||
|
||||
# rrule was: DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000
|
||||
# rrule is now: DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T220000Z
|
||||
rrule = rrule.replace(naive_until, utc_until)
|
||||
return rrule
|
||||
|
||||
@classmethod
|
||||
def rrulestr(cls, rrule, **kwargs):
|
||||
"""
|
||||
Apply our own custom rrule parsing requirements
|
||||
"""
|
||||
rrule = Schedule.coerce_naive_until(rrule)
|
||||
kwargs['forceset'] = True
|
||||
x = dateutil.rrule.rrulestr(rrule, **kwargs)
|
||||
x = dateutil.rrule.rrulestr(rrule, tzinfos=UTC_TIMEZONES, **kwargs)
|
||||
|
||||
for r in x._rrule:
|
||||
if r._dtstart and r._dtstart.tzinfo is None:
|
||||
@ -158,4 +250,5 @@ class Schedule(CommonModel, LaunchTimeConfig):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.update_computed_fields()
|
||||
self.rrule = Schedule.coerce_naive_until(self.rrule)
|
||||
super(Schedule, self).save(*args, **kwargs)
|
||||
|
||||
@ -838,8 +838,11 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
setattr(unified_job, fd, val)
|
||||
unified_job.save()
|
||||
|
||||
# Labels coppied here
|
||||
copy_m2m_relationships(self, unified_job, fields)
|
||||
# Labels copied here
|
||||
from awx.main.signals import disable_activity_stream
|
||||
with disable_activity_stream():
|
||||
copy_m2m_relationships(self, unified_job, fields)
|
||||
|
||||
return unified_job
|
||||
|
||||
def launch_prompts(self):
|
||||
|
||||
@ -205,7 +205,7 @@ def set_original_organization(sender, instance, **kwargs):
|
||||
pre-save organization, so we can later determine if the organization
|
||||
field is dirty.
|
||||
'''
|
||||
instance.__original_org = instance.organization
|
||||
instance.__original_org_id = instance.organization_id
|
||||
|
||||
|
||||
def save_related_job_templates(sender, instance, **kwargs):
|
||||
@ -217,7 +217,7 @@ def save_related_job_templates(sender, instance, **kwargs):
|
||||
if sender not in (Project, Inventory):
|
||||
raise ValueError('This signal callback is only intended for use with Project or Inventory')
|
||||
|
||||
if instance.__original_org != instance.organization:
|
||||
if instance.__original_org_id != instance.organization_id:
|
||||
jtq = JobTemplate.objects.filter(**{sender.__name__.lower(): instance})
|
||||
for jt in jtq:
|
||||
update_role_parentage_for_instance(jt)
|
||||
@ -494,6 +494,8 @@ def activity_stream_delete(sender, instance, **kwargs):
|
||||
return
|
||||
changes = model_to_dict(instance)
|
||||
object1 = camelcase_to_underscore(instance.__class__.__name__)
|
||||
if type(instance) == OAuth2AccessToken:
|
||||
changes['token'] = TOKEN_CENSOR
|
||||
activity_entry = ActivityStream(
|
||||
operation='delete',
|
||||
changes=json.dumps(changes),
|
||||
|
||||
@ -29,7 +29,7 @@ except Exception:
|
||||
|
||||
# Celery
|
||||
from celery import Task, shared_task, Celery
|
||||
from celery.signals import celeryd_init, worker_process_init, worker_shutdown, worker_ready, celeryd_after_setup
|
||||
from celery.signals import celeryd_init, worker_shutdown, worker_ready, celeryd_after_setup
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
@ -49,6 +49,7 @@ from crum import impersonate
|
||||
# AWX
|
||||
from awx import __version__ as awx_application_version
|
||||
from awx.main.constants import CLOUD_PROVIDERS, PRIVILEGE_ESCALATION_METHODS
|
||||
from awx.main.access import access_registry
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.constants import ACTIVE_STATES
|
||||
from awx.main.exceptions import AwxTaskError
|
||||
@ -59,13 +60,15 @@ from awx.main.utils import (get_ansible_version, get_ssh_version, decrypt_field,
|
||||
wrap_args_with_proot, OutputEventFilter, OutputVerboseFilter, ignore_inventory_computed_fields,
|
||||
ignore_inventory_group_removal, get_type_for_model, extract_ansible_vars)
|
||||
from awx.main.utils.safe_yaml import safe_dump, sanitize_jinja
|
||||
from awx.main.utils.reload import restart_local_services, stop_local_services
|
||||
from awx.main.utils.reload import stop_local_services
|
||||
from awx.main.utils.pglock import advisory_lock
|
||||
from awx.main.utils.ha import update_celery_worker_routes, register_celery_worker_queues
|
||||
from awx.main.utils.handlers import configure_external_logger
|
||||
from awx.main.utils.ha import register_celery_worker_queues
|
||||
from awx.main.consumers import emit_channel_notification
|
||||
from awx.conf import settings_registry
|
||||
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
|
||||
__all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate',
|
||||
'RunAdHocCommand', 'handle_work_error', 'handle_work_success', 'apply_cluster_membership_policies',
|
||||
'update_inventory_computed_fields', 'update_host_smart_inventory_memberships',
|
||||
@ -117,15 +120,6 @@ def celery_startup(conf=None, **kwargs):
|
||||
logger.exception(six.text_type("Failed to rebuild schedule {}.").format(sch))
|
||||
|
||||
|
||||
@worker_process_init.connect
|
||||
def task_set_logger_pre_run(*args, **kwargs):
|
||||
try:
|
||||
cache.close()
|
||||
configure_external_logger(settings, is_startup=False)
|
||||
except Exception:
|
||||
logger.exception('Encountered error on initial log configuration.')
|
||||
|
||||
|
||||
@worker_shutdown.connect
|
||||
def inform_cluster_of_shutdown(*args, **kwargs):
|
||||
try:
|
||||
@ -152,7 +146,7 @@ def apply_cluster_membership_policies(self):
|
||||
# Process policy instance list first, these will represent manually managed instances
|
||||
# that will not go through automatic policy determination
|
||||
for ig in InstanceGroup.objects.all():
|
||||
logger.info(six.text_type("Considering group {}").format(ig.name))
|
||||
logger.info(six.text_type("Applying cluster membership policies to Group {}").format(ig.name))
|
||||
ig.instances.clear()
|
||||
group_actual = Group(obj=ig, instances=[])
|
||||
for i in ig.policy_instance_list:
|
||||
@ -160,7 +154,7 @@ def apply_cluster_membership_policies(self):
|
||||
if not inst.exists():
|
||||
continue
|
||||
inst = inst[0]
|
||||
logger.info(six.text_type("Policy List, adding {} to {}").format(inst.hostname, ig.name))
|
||||
logger.info(six.text_type("Policy List, adding Instance {} to Group {}").format(inst.hostname, ig.name))
|
||||
group_actual.instances.append(inst.id)
|
||||
ig.instances.add(inst)
|
||||
filtered_instances.append(inst)
|
||||
@ -173,7 +167,7 @@ def apply_cluster_membership_policies(self):
|
||||
for i in sorted(actual_instances, cmp=lambda x,y: len(x.groups) - len(y.groups)):
|
||||
if len(g.instances) >= g.obj.policy_instance_minimum:
|
||||
break
|
||||
logger.info(six.text_type("Policy minimum, adding {} to {}").format(i.obj.hostname, g.obj.name))
|
||||
logger.info(six.text_type("Policy minimum, adding Instance {} to Group {}").format(i.obj.hostname, g.obj.name))
|
||||
g.obj.instances.add(i.obj)
|
||||
g.instances.append(i.obj.id)
|
||||
i.groups.append(g.obj.id)
|
||||
@ -182,14 +176,14 @@ def apply_cluster_membership_policies(self):
|
||||
for i in sorted(actual_instances, cmp=lambda x,y: len(x.groups) - len(y.groups)):
|
||||
if 100 * float(len(g.instances)) / len(actual_instances) >= g.obj.policy_instance_percentage:
|
||||
break
|
||||
logger.info(six.text_type("Policy percentage, adding {} to {}").format(i.obj.hostname, g.obj.name))
|
||||
logger.info(six.text_type("Policy percentage, adding Instance {} to Group {}").format(i.obj.hostname, g.obj.name))
|
||||
g.instances.append(i.obj.id)
|
||||
g.obj.instances.add(i.obj)
|
||||
i.groups.append(g.obj.id)
|
||||
handle_ha_toplogy_changes.apply([])
|
||||
|
||||
|
||||
@shared_task(queue='tower_broadcast_all', bind=True)
|
||||
@shared_task(exchange='tower_broadcast_all', bind=True)
|
||||
def handle_setting_changes(self, setting_keys):
|
||||
orig_len = len(setting_keys)
|
||||
for i in range(orig_len):
|
||||
@ -200,15 +194,9 @@ def handle_setting_changes(self, setting_keys):
|
||||
cache_keys = set(setting_keys)
|
||||
logger.debug('cache delete_many(%r)', cache_keys)
|
||||
cache.delete_many(cache_keys)
|
||||
for key in cache_keys:
|
||||
if key.startswith('LOG_AGGREGATOR_'):
|
||||
restart_local_services(['uwsgi', 'celery', 'beat', 'callback'])
|
||||
break
|
||||
elif key == 'OAUTH2_PROVIDER':
|
||||
restart_local_services(['uwsgi'])
|
||||
|
||||
|
||||
@shared_task(bind=True, queue='tower_broadcast_all')
|
||||
@shared_task(bind=True, exchange='tower_broadcast_all')
|
||||
def handle_ha_toplogy_changes(self):
|
||||
(changed, instance) = Instance.objects.get_or_register()
|
||||
if changed:
|
||||
@ -217,39 +205,24 @@ def handle_ha_toplogy_changes(self):
|
||||
awx_app = Celery('awx')
|
||||
awx_app.config_from_object('django.conf:settings')
|
||||
instances, removed_queues, added_queues = register_celery_worker_queues(awx_app, self.request.hostname)
|
||||
for instance in instances:
|
||||
logger.info(six.text_type("Workers on tower node '{}' removed from queues {} and added to queues {}")
|
||||
.format(instance.hostname, removed_queues, added_queues))
|
||||
updated_routes = update_celery_worker_routes(instance, settings)
|
||||
logger.info(six.text_type("Worker on tower node '{}' updated celery routes {} all routes are now {}")
|
||||
.format(instance.hostname, updated_routes, self.app.conf.CELERY_ROUTES))
|
||||
if len(removed_queues) + len(added_queues) > 0:
|
||||
logger.info(six.text_type("Workers on tower node(s) '{}' removed from queues {} and added to queues {}")
|
||||
.format([i.hostname for i in instances], removed_queues, added_queues))
|
||||
|
||||
|
||||
@worker_ready.connect
|
||||
def handle_ha_toplogy_worker_ready(sender, **kwargs):
|
||||
logger.debug(six.text_type("Configure celeryd queues task on host {}").format(sender.hostname))
|
||||
instances, removed_queues, added_queues = register_celery_worker_queues(sender.app, sender.hostname)
|
||||
for instance in instances:
|
||||
logger.info(six.text_type("Workers on tower node '{}' unsubscribed from queues {} and subscribed to queues {}")
|
||||
.format(instance.hostname, removed_queues, added_queues))
|
||||
if len(removed_queues) + len(added_queues) > 0:
|
||||
logger.info(six.text_type("Workers on tower node(s) '{}' removed from queues {} and added to queues {}")
|
||||
.format([i.hostname for i in instances], removed_queues, added_queues))
|
||||
|
||||
# Expedite the first hearbeat run so a node comes online quickly.
|
||||
cluster_node_heartbeat.apply([])
|
||||
apply_cluster_membership_policies.apply([])
|
||||
|
||||
|
||||
@celeryd_init.connect
|
||||
def handle_update_celery_routes(sender=None, conf=None, **kwargs):
|
||||
conf = conf if conf else sender.app.conf
|
||||
logger.debug(six.text_type("Registering celery routes for {}").format(sender))
|
||||
(changed, instance) = Instance.objects.get_or_register()
|
||||
if changed:
|
||||
logger.info(six.text_type("Registered tower node '{}'").format(instance.hostname))
|
||||
added_routes = update_celery_worker_routes(instance, conf)
|
||||
logger.info(six.text_type("Workers on tower node '{}' added routes {} all routes are now {}")
|
||||
.format(instance.hostname, added_routes, conf.CELERY_ROUTES))
|
||||
|
||||
|
||||
@celeryd_after_setup.connect
|
||||
def handle_update_celery_hostname(sender, instance, **kwargs):
|
||||
(changed, tower_instance) = Instance.objects.get_or_register()
|
||||
@ -282,7 +255,10 @@ def send_notifications(notification_list, job_id=None):
|
||||
notification.error = smart_str(e)
|
||||
update_fields.append('error')
|
||||
finally:
|
||||
notification.save(update_fields=update_fields)
|
||||
try:
|
||||
notification.save(update_fields=update_fields)
|
||||
except Exception as e:
|
||||
logger.exception(six.text_type('Error saving notification {} result.').format(notification.id))
|
||||
|
||||
|
||||
@shared_task(bind=True, queue=settings.CELERY_DEFAULT_QUEUE)
|
||||
@ -426,6 +402,13 @@ def awx_periodic_scheduler(self):
|
||||
for schedule in old_schedules:
|
||||
schedule.save()
|
||||
schedules = Schedule.objects.enabled().between(last_run, run_now)
|
||||
|
||||
invalid_license = False
|
||||
try:
|
||||
access_registry[Job](None).check_license()
|
||||
except PermissionDenied as e:
|
||||
invalid_license = e
|
||||
|
||||
for schedule in schedules:
|
||||
template = schedule.unified_job_template
|
||||
schedule.save() # To update next_run timestamp.
|
||||
@ -435,6 +418,13 @@ def awx_periodic_scheduler(self):
|
||||
try:
|
||||
job_kwargs = schedule.get_job_kwargs()
|
||||
new_unified_job = schedule.unified_job_template.create_unified_job(**job_kwargs)
|
||||
|
||||
if invalid_license:
|
||||
new_unified_job.status = 'failed'
|
||||
new_unified_job.job_explanation = str(invalid_license)
|
||||
new_unified_job.save(update_fields=['status', 'job_explanation'])
|
||||
new_unified_job.websocket_emit_status("failed")
|
||||
raise invalid_license
|
||||
can_start = new_unified_job.signal_start()
|
||||
except Exception:
|
||||
logger.exception('Error spawning scheduled job.')
|
||||
@ -561,6 +551,8 @@ def delete_inventory(self, inventory_id, user_id):
|
||||
with ignore_inventory_computed_fields(), ignore_inventory_group_removal(), impersonate(user):
|
||||
try:
|
||||
i = Inventory.objects.get(id=inventory_id)
|
||||
for host in i.hosts.iterator():
|
||||
host.job_events_as_primary_host.update(host=None)
|
||||
i.delete()
|
||||
emit_channel_notification(
|
||||
'inventories-status_changed',
|
||||
@ -1677,7 +1669,13 @@ class RunProjectUpdate(BaseTask):
|
||||
raise
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
fcntl.flock(self.lock_fd, fcntl.LOCK_EX)
|
||||
waiting_time = time.time() - start_time
|
||||
if waiting_time > 1.0:
|
||||
logger.info(six.text_type(
|
||||
'{} spent {} waiting to acquire lock for local source tree '
|
||||
'for path {}.').format(instance.log_format, waiting_time, lock_path))
|
||||
except IOError as e:
|
||||
os.close(self.lock_fd)
|
||||
logger.error(six.text_type("I/O error({0}) while trying to aquire lock on file [{1}]: {2}").format(e.errno, lock_path, e.strerror))
|
||||
@ -1725,6 +1723,10 @@ class RunInventoryUpdate(BaseTask):
|
||||
event_model = InventoryUpdateEvent
|
||||
event_data_key = 'inventory_update_id'
|
||||
|
||||
@property
|
||||
def proot_show_paths(self):
|
||||
return [self.get_path_to('..', 'plugins', 'inventory')]
|
||||
|
||||
def build_private_data(self, inventory_update, **kwargs):
|
||||
"""
|
||||
Return private data needed for inventory update.
|
||||
@ -2080,6 +2082,8 @@ class RunInventoryUpdate(BaseTask):
|
||||
return args
|
||||
|
||||
def build_cwd(self, inventory_update, **kwargs):
|
||||
if inventory_update.source == 'scm' and inventory_update.source_project_update:
|
||||
return inventory_update.source_project_update.get_project_path(check_if_exists=False)
|
||||
return self.get_path_to('..', 'plugins', 'inventory')
|
||||
|
||||
def get_idle_timeout(self):
|
||||
@ -2331,6 +2335,9 @@ def _reconstruct_relationships(copy_mapping):
|
||||
setattr(new_obj, field_name, related_obj)
|
||||
elif field.many_to_many:
|
||||
for related_obj in getattr(old_obj, field_name).all():
|
||||
logger.debug(six.text_type('Deep copy: Adding {} to {}({}).{} relationship').format(
|
||||
related_obj, new_obj, model, field_name
|
||||
))
|
||||
getattr(new_obj, field_name).add(copy_mapping.get(related_obj, related_obj))
|
||||
new_obj.save()
|
||||
|
||||
@ -2352,7 +2359,7 @@ def deep_copy_model_obj(
|
||||
except ObjectDoesNotExist:
|
||||
logger.warning("Object or user no longer exists.")
|
||||
return
|
||||
with transaction.atomic():
|
||||
with transaction.atomic(), ignore_inventory_computed_fields():
|
||||
copy_mapping = {}
|
||||
for sub_obj_setup in sub_obj_list:
|
||||
sub_model = getattr(importlib.import_module(sub_obj_setup[0]),
|
||||
@ -2372,3 +2379,5 @@ def deep_copy_model_obj(
|
||||
importlib.import_module(permission_check_func[0]), permission_check_func[1]
|
||||
), permission_check_func[2])
|
||||
permission_check_func(creater, copy_mapping.values())
|
||||
if isinstance(new_obj, Inventory):
|
||||
update_inventory_computed_fields.delay(new_obj.id, True)
|
||||
|
||||
@ -15,6 +15,16 @@ from awx.main.tests.factories import (
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
import sys
|
||||
sys._called_from_test = True
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
import sys
|
||||
del sys._called_from_test
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_access():
|
||||
@contextmanager
|
||||
@ -96,3 +106,21 @@ def get_ssh_version(mocker):
|
||||
@pytest.fixture
|
||||
def job_template_with_survey_passwords_unit(job_template_with_survey_passwords_factory):
|
||||
return job_template_with_survey_passwords_factory(persisted=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_cache():
|
||||
class MockCache(object):
|
||||
cache = {}
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self.cache.get(key, default)
|
||||
|
||||
def set(self, key, value, timeout=60):
|
||||
self.cache[key] = value
|
||||
|
||||
def delete(self, key):
|
||||
del self.cache[key]
|
||||
|
||||
return MockCache()
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from django.db import connection
|
||||
from django.db.models.signals import post_migrate
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def app_post_migration(sender, app_config, **kwargs):
|
||||
@ -17,7 +18,8 @@ def app_post_migration(sender, app_config, **kwargs):
|
||||
)
|
||||
|
||||
|
||||
post_migrate.connect(app_post_migration, sender=apps.get_app_config('main'))
|
||||
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3':
|
||||
post_migrate.connect(app_post_migration, sender=apps.get_app_config('main'))
|
||||
|
||||
|
||||
|
||||
|
||||
@ -94,10 +94,16 @@ class TestDeleteViews:
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_non_filterable_field(options, instance, admin_user):
|
||||
def test_filterable_fields(options, instance, admin_user):
|
||||
r = options(
|
||||
url=instance.get_absolute_url(),
|
||||
user=admin_user
|
||||
)
|
||||
field_info = r.data['actions']['GET']['percent_capacity_remaining']
|
||||
assert 'filterable' in field_info
|
||||
|
||||
filterable_info = r.data['actions']['GET']['created']
|
||||
non_filterable_info = r.data['actions']['GET']['percent_capacity_remaining']
|
||||
|
||||
assert 'filterable' in filterable_info
|
||||
assert filterable_info['filterable'] is True
|
||||
|
||||
assert 'filterable' not in non_filterable_info
|
||||
|
||||
@ -87,7 +87,7 @@ def test_delete_instance_group_jobs_running(delete, instance_group_jobs_running,
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_modify_delete_tower_instance_group_prevented(delete, options, tower_instance_group, user, patch, put):
|
||||
def test_delete_rename_tower_instance_group_prevented(delete, options, tower_instance_group, instance_group, user, patch):
|
||||
url = reverse("api:instance_group_detail", kwargs={'pk': tower_instance_group.pk})
|
||||
super_user = user('bob', True)
|
||||
|
||||
@ -99,6 +99,13 @@ def test_modify_delete_tower_instance_group_prevented(delete, options, tower_ins
|
||||
assert 'GET' in resp.data['actions']
|
||||
assert 'PUT' in resp.data['actions']
|
||||
|
||||
# Rename 'tower' instance group denied
|
||||
patch(url, {'name': 'tower_prime'}, super_user, expect=400)
|
||||
|
||||
# Rename, other instance group OK
|
||||
url = reverse("api:instance_group_detail", kwargs={'pk': instance_group.pk})
|
||||
patch(url, {'name': 'foobar'}, super_user, expect=200)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_prevent_delete_iso_and_control_groups(delete, isolated_instance_group, admin):
|
||||
|
||||
@ -126,9 +126,8 @@ def test_list_cannot_order_by_unsearchable_field(get, organization, alice, order
|
||||
)
|
||||
custom_script.admin_role.members.add(alice)
|
||||
|
||||
response = get(reverse('api:inventory_script_list'), alice,
|
||||
QUERY_STRING='order_by=%s' % order_by, status=400)
|
||||
assert response.status_code == 400
|
||||
get(reverse('api:inventory_script_list'), alice,
|
||||
QUERY_STRING='order_by=%s' % order_by, expect=403)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role_field,expected_status_code", [
|
||||
|
||||
@ -625,17 +625,31 @@ def test_save_survey_passwords_on_migration(job_template_with_survey_passwords):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_job_template_custom_virtualenv(get, patch, organization_factory, job_template_factory):
|
||||
@pytest.mark.parametrize('access', ["superuser", "admin", "peon"])
|
||||
def test_job_template_custom_virtualenv(get, patch, organization_factory, job_template_factory, alice, access):
|
||||
objs = organization_factory("org", superusers=['admin'])
|
||||
jt = job_template_factory("jt", organization=objs.organization,
|
||||
inventory='test_inv', project='test_proj').job_template
|
||||
|
||||
user = alice
|
||||
if access == "superuser":
|
||||
user = objs.superusers.admin
|
||||
elif access == "admin":
|
||||
jt.admin_role.members.add(alice)
|
||||
else:
|
||||
jt.read_role.members.add(alice)
|
||||
|
||||
with TemporaryDirectory(dir=settings.BASE_VENV_PATH) as temp_dir:
|
||||
admin = objs.superusers.admin
|
||||
os.makedirs(os.path.join(temp_dir, 'bin', 'activate'))
|
||||
url = reverse('api:job_template_detail', kwargs={'pk': jt.id})
|
||||
patch(url, {'custom_virtualenv': temp_dir}, user=admin, expect=200)
|
||||
assert get(url, user=admin).data['custom_virtualenv'] == os.path.join(temp_dir, '')
|
||||
|
||||
if access == "peon":
|
||||
patch(url, {'custom_virtualenv': temp_dir}, user=user, expect=403)
|
||||
assert 'custom_virtualenv' not in get(url, user=user)
|
||||
assert JobTemplate.objects.get(pk=jt.id).custom_virtualenv is None
|
||||
else:
|
||||
patch(url, {'custom_virtualenv': temp_dir}, user=user, expect=200)
|
||||
assert get(url, user=user).data['custom_virtualenv'] == os.path.join(temp_dir, '')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@ -172,3 +172,12 @@ def test_oauth_application_delete(oauth_application, post, delete, admin):
|
||||
assert Application.objects.filter(client_id=oauth_application.client_id).count() == 0
|
||||
assert RefreshToken.objects.filter(application=oauth_application).count() == 0
|
||||
assert AccessToken.objects.filter(application=oauth_application).count() == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_oauth_list_user_tokens(oauth_application, post, get, admin, alice):
|
||||
for user in (admin, alice):
|
||||
url = reverse('api:o_auth2_token_list', kwargs={'pk': user.pk})
|
||||
post(url, {'scope': 'read'}, user, expect=201)
|
||||
response = get(url, admin, expect=200)
|
||||
assert response.data['count'] == 1
|
||||
|
||||
@ -14,7 +14,7 @@ import mock
|
||||
# AWX
|
||||
from awx.api.versioning import reverse
|
||||
from awx.conf.models import Setting
|
||||
from awx.main.utils.handlers import BaseHTTPSHandler, LoggingConnectivityException
|
||||
from awx.main.utils.handlers import AWXProxyHandler, LoggingConnectivityException
|
||||
|
||||
import six
|
||||
|
||||
@ -217,7 +217,7 @@ def test_logging_aggregrator_connection_test_bad_request(get, post, admin, key):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_logging_aggregrator_connection_test_valid(mocker, get, post, admin):
|
||||
with mock.patch.object(BaseHTTPSHandler, 'perform_test') as perform_test:
|
||||
with mock.patch.object(AWXProxyHandler, 'perform_test') as perform_test:
|
||||
url = reverse('api:setting_logging_test')
|
||||
user_data = {
|
||||
'LOG_AGGREGATOR_TYPE': 'logstash',
|
||||
@ -227,7 +227,8 @@ def test_logging_aggregrator_connection_test_valid(mocker, get, post, admin):
|
||||
'LOG_AGGREGATOR_PASSWORD': 'mcstash'
|
||||
}
|
||||
post(url, user_data, user=admin, expect=200)
|
||||
create_settings = perform_test.call_args[0][0]
|
||||
args, kwargs = perform_test.call_args_list[0]
|
||||
create_settings = kwargs['custom_settings']
|
||||
for k, v in user_data.items():
|
||||
assert hasattr(create_settings, k)
|
||||
assert getattr(create_settings, k) == v
|
||||
@ -238,7 +239,7 @@ def test_logging_aggregrator_connection_test_with_masked_password(mocker, patch,
|
||||
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'logging'})
|
||||
patch(url, user=admin, data={'LOG_AGGREGATOR_PASSWORD': 'password123'}, expect=200)
|
||||
|
||||
with mock.patch.object(BaseHTTPSHandler, 'perform_test') as perform_test:
|
||||
with mock.patch.object(AWXProxyHandler, 'perform_test') as perform_test:
|
||||
url = reverse('api:setting_logging_test')
|
||||
user_data = {
|
||||
'LOG_AGGREGATOR_TYPE': 'logstash',
|
||||
@ -248,13 +249,14 @@ def test_logging_aggregrator_connection_test_with_masked_password(mocker, patch,
|
||||
'LOG_AGGREGATOR_PASSWORD': '$encrypted$'
|
||||
}
|
||||
post(url, user_data, user=admin, expect=200)
|
||||
create_settings = perform_test.call_args[0][0]
|
||||
args, kwargs = perform_test.call_args_list[0]
|
||||
create_settings = kwargs['custom_settings']
|
||||
assert getattr(create_settings, 'LOG_AGGREGATOR_PASSWORD') == 'password123'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_logging_aggregrator_connection_test_invalid(mocker, get, post, admin):
|
||||
with mock.patch.object(BaseHTTPSHandler, 'perform_test') as perform_test:
|
||||
with mock.patch.object(AWXProxyHandler, 'perform_test') as perform_test:
|
||||
perform_test.side_effect = LoggingConnectivityException('404: Not Found')
|
||||
url = reverse('api:setting_logging_test')
|
||||
resp = post(url, {
|
||||
|
||||
@ -8,6 +8,7 @@ import tempfile
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.backends.sqlite3.base import SQLiteCursorWrapper
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
from awx.api.versioning import reverse
|
||||
@ -184,6 +185,7 @@ def test_text_stdout_with_max_stdout(sqlite_copy_expert, get, admin):
|
||||
[_mk_inventory_update, InventoryUpdateEvent, 'inventory_update', 'api:inventory_update_stdout'],
|
||||
])
|
||||
@pytest.mark.parametrize('fmt', ['txt', 'ansi'])
|
||||
@mock.patch('awx.main.redact.UriCleaner.SENSITIVE_URI_PATTERN', mock.Mock(**{'search.return_value': None})) # really slow for large strings
|
||||
def test_max_bytes_display(sqlite_copy_expert, Parent, Child, relation, view, fmt, get, admin):
|
||||
job = Parent()
|
||||
job.save()
|
||||
@ -231,6 +233,7 @@ def test_legacy_result_stdout_text_fallback(Cls, view, fmt, get, admin):
|
||||
[_mk_inventory_update, 'api:inventory_update_stdout']
|
||||
])
|
||||
@pytest.mark.parametrize('fmt', ['txt', 'ansi'])
|
||||
@mock.patch('awx.main.redact.UriCleaner.SENSITIVE_URI_PATTERN', mock.Mock(**{'search.return_value': None})) # really slow for large strings
|
||||
def test_legacy_result_stdout_with_max_bytes(Cls, view, fmt, get, admin):
|
||||
job = Cls()
|
||||
job.save()
|
||||
@ -282,7 +285,7 @@ def test_unicode_with_base64_ansi(sqlite_copy_expert, get, admin):
|
||||
url = reverse(
|
||||
'api:job_stdout',
|
||||
kwargs={'pk': job.pk}
|
||||
) + '?format=json&content_encoding=base64&content_format=ansi'
|
||||
) + '?format=json&content_encoding=base64'
|
||||
|
||||
response = get(url, user=admin, expect=200)
|
||||
content = base64.b64decode(json.loads(response.content)['content'])
|
||||
|
||||
@ -9,7 +9,6 @@ from six.moves import xrange
|
||||
|
||||
# Django
|
||||
from django.core.urlresolvers import resolve
|
||||
from django.core.cache import cache
|
||||
from django.utils.six.moves.urllib.parse import urlparse
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.models import User
|
||||
@ -57,14 +56,6 @@ def swagger_autogen(requests=__SWAGGER_REQUESTS__):
|
||||
return requests
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_cache():
|
||||
'''
|
||||
Clear cache (local memory) for each test to prevent using cached settings.
|
||||
'''
|
||||
cache.clear()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def celery_memory_broker():
|
||||
'''
|
||||
|
||||
33
awx/main/tests/functional/models/test_project.py
Normal file
33
awx/main/tests/functional/models/test_project.py
Normal file
@ -0,0 +1,33 @@
|
||||
import pytest
|
||||
import mock
|
||||
|
||||
from awx.main.models import Project
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_project_initial_update():
|
||||
with mock.patch.object(Project, "update") as mock_update:
|
||||
Project.objects.create(name='foo', scm_type='git')
|
||||
mock_update.assert_called_once_with()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_does_not_update_nonsensitive_change(project):
|
||||
with mock.patch.object(Project, "update") as mock_update:
|
||||
project.scm_update_on_launch = not project.scm_update_on_launch
|
||||
project.save()
|
||||
mock_update.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_sensitive_change_triggers_update(project):
|
||||
with mock.patch.object(Project, "update") as mock_update:
|
||||
project.scm_url = 'https://foo.invalid'
|
||||
project.save()
|
||||
mock_update.assert_called_once_with()
|
||||
# test other means of initialization
|
||||
project = Project.objects.get(pk=project.pk)
|
||||
with mock.patch.object(Project, "update") as mock_update:
|
||||
project.scm_url = 'https://foo2.invalid'
|
||||
project.save()
|
||||
mock_update.assert_called_once_with()
|
||||
@ -1,5 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.utils.timezone import now
|
||||
import mock
|
||||
import pytest
|
||||
import pytz
|
||||
@ -131,31 +132,19 @@ def test_utc_until(job_template, until, dtend):
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('dtstart, until', [
|
||||
['20180601T120000Z', '20180602T170000'],
|
||||
['TZID=America/New_York:20180601T120000', '20180602T170000'],
|
||||
['DTSTART:20380601T120000Z', '20380601T170000'], # noon UTC to 5PM UTC
|
||||
['DTSTART;TZID=America/New_York:20380601T120000', '20380601T170000'], # noon EST to 5PM EST
|
||||
])
|
||||
def test_tzinfo_naive_until(job_template, dtstart, until):
|
||||
rrule = 'DTSTART;{} RRULE:FREQ=DAILY;INTERVAL=1;UNTIL={}'.format(dtstart, until) # noqa
|
||||
rrule = '{} RRULE:FREQ=HOURLY;INTERVAL=1;UNTIL={}'.format(dtstart, until) # noqa
|
||||
s = Schedule(
|
||||
name='Some Schedule',
|
||||
rrule=rrule,
|
||||
unified_job_template=job_template
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
s.save()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_until_must_be_utc(job_template):
|
||||
rrule = 'DTSTART;TZID=America/New_York:20180601T120000 RRULE:FREQ=DAILY;INTERVAL=1;UNTIL=20180602T000000' # noqa the Z is required
|
||||
s = Schedule(
|
||||
name='Some Schedule',
|
||||
rrule=rrule,
|
||||
unified_job_template=job_template
|
||||
)
|
||||
with pytest.raises(ValueError) as e:
|
||||
s.save()
|
||||
assert 'RRULE UNTIL values must be specified in UTC' in str(e)
|
||||
s.save()
|
||||
gen = Schedule.rrulestr(s.rrule).xafter(now(), count=20)
|
||||
assert len(list(gen)) == 6 # noon, 1PM, 2, 3, 4, 5PM
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@ -203,3 +192,85 @@ def test_beginning_of_time(job_template):
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
s.save()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('rrule, tz', [
|
||||
['DTSTART:20300112T210000Z RRULE:FREQ=DAILY;INTERVAL=1', 'UTC'],
|
||||
['DTSTART;TZID=America/New_York:20300112T210000 RRULE:FREQ=DAILY;INTERVAL=1', 'America/New_York']
|
||||
])
|
||||
def test_timezone_property(job_template, rrule, tz):
|
||||
s = Schedule(
|
||||
name='Some Schedule',
|
||||
rrule=rrule,
|
||||
unified_job_template=job_template
|
||||
)
|
||||
assert s.timezone == tz
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_utc_until_property(job_template):
|
||||
rrule = 'DTSTART:20380601T120000Z RRULE:FREQ=HOURLY;INTERVAL=1;UNTIL=20380601T170000Z'
|
||||
s = Schedule(
|
||||
name='Some Schedule',
|
||||
rrule=rrule,
|
||||
unified_job_template=job_template
|
||||
)
|
||||
s.save()
|
||||
|
||||
assert s.rrule.endswith('20380601T170000Z')
|
||||
assert s.until == '2038-06-01T17:00:00'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_localized_until_property(job_template):
|
||||
rrule = 'DTSTART;TZID=America/New_York:20380601T120000 RRULE:FREQ=HOURLY;INTERVAL=1;UNTIL=20380601T220000Z'
|
||||
s = Schedule(
|
||||
name='Some Schedule',
|
||||
rrule=rrule,
|
||||
unified_job_template=job_template
|
||||
)
|
||||
s.save()
|
||||
|
||||
assert s.rrule.endswith('20380601T220000Z')
|
||||
assert s.until == '2038-06-01T17:00:00'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_utc_naive_coercion(job_template):
|
||||
rrule = 'DTSTART:20380601T120000Z RRULE:FREQ=HOURLY;INTERVAL=1;UNTIL=20380601T170000'
|
||||
s = Schedule(
|
||||
name='Some Schedule',
|
||||
rrule=rrule,
|
||||
unified_job_template=job_template
|
||||
)
|
||||
s.save()
|
||||
|
||||
assert s.rrule.endswith('20380601T170000Z')
|
||||
assert s.until == '2038-06-01T17:00:00'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_est_naive_coercion(job_template):
|
||||
rrule = 'DTSTART;TZID=America/New_York:20380601T120000 RRULE:FREQ=HOURLY;INTERVAL=1;UNTIL=20380601T170000'
|
||||
s = Schedule(
|
||||
name='Some Schedule',
|
||||
rrule=rrule,
|
||||
unified_job_template=job_template
|
||||
)
|
||||
s.save()
|
||||
|
||||
assert s.rrule.endswith('20380601T220000Z') # 5PM EDT = 10PM UTC
|
||||
assert s.until == '2038-06-01T17:00:00'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_empty_until_property(job_template):
|
||||
rrule = 'DTSTART;TZID=America/New_York:20380601T120000 RRULE:FREQ=HOURLY;INTERVAL=1'
|
||||
s = Schedule(
|
||||
name='Some Schedule',
|
||||
rrule=rrule,
|
||||
unified_job_template=job_template
|
||||
)
|
||||
s.save()
|
||||
assert s.until == ''
|
||||
|
||||
@ -113,7 +113,7 @@ def test_single_job_dependencies_project_launch(default_instance_group, job_temp
|
||||
p.scm_update_cache_timeout = 0
|
||||
p.scm_type = "git"
|
||||
p.scm_url = "http://github.com/ansible/ansible.git"
|
||||
p.save()
|
||||
p.save(skip_update=True)
|
||||
with mock.patch("awx.main.scheduler.TaskManager.start_task"):
|
||||
tm = TaskManager()
|
||||
with mock.patch.object(TaskManager, "create_project_update", wraps=tm.create_project_update) as mock_pu:
|
||||
@ -241,15 +241,16 @@ def test_shared_dependencies_launch(default_instance_group, job_template_factory
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cleanup_interval():
|
||||
assert cache.get('last_celery_task_cleanup') is None
|
||||
def test_cleanup_interval(mock_cache):
|
||||
with mock.patch.multiple('awx.main.scheduler.task_manager.cache', get=mock_cache.get, set=mock_cache.set):
|
||||
assert mock_cache.get('last_celery_task_cleanup') is None
|
||||
|
||||
TaskManager().cleanup_inconsistent_celery_tasks()
|
||||
last_cleanup = cache.get('last_celery_task_cleanup')
|
||||
assert isinstance(last_cleanup, datetime)
|
||||
TaskManager().cleanup_inconsistent_celery_tasks()
|
||||
last_cleanup = mock_cache.get('last_celery_task_cleanup')
|
||||
assert isinstance(last_cleanup, datetime)
|
||||
|
||||
TaskManager().cleanup_inconsistent_celery_tasks()
|
||||
assert cache.get('last_celery_task_cleanup') == last_cleanup
|
||||
TaskManager().cleanup_inconsistent_celery_tasks()
|
||||
assert cache.get('last_celery_task_cleanup') == last_cleanup
|
||||
|
||||
|
||||
class TestReaper():
|
||||
@ -326,7 +327,8 @@ class TestReaper():
|
||||
@pytest.mark.django_db
|
||||
@mock.patch.object(JobNotificationMixin, 'send_notification_templates')
|
||||
@mock.patch.object(TaskManager, 'get_active_tasks', lambda self: ([], []))
|
||||
def test_cleanup_inconsistent_task(self, notify, active_tasks, considered_jobs, reapable_jobs, running_tasks, waiting_tasks, mocker):
|
||||
def test_cleanup_inconsistent_task(self, notify, active_tasks, considered_jobs, reapable_jobs, running_tasks, waiting_tasks, mocker, settings):
|
||||
settings.AWX_INCONSISTENT_TASK_INTERVAL = 0
|
||||
tm = TaskManager()
|
||||
|
||||
tm.get_running_tasks = mocker.Mock(return_value=(running_tasks, waiting_tasks))
|
||||
|
||||
@ -5,6 +5,7 @@ from awx.main.models import (
|
||||
Organization,
|
||||
Project,
|
||||
)
|
||||
from awx.main.fields import update_role_parentage_for_instance
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@ -202,3 +203,11 @@ def test_auto_parenting():
|
||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role)
|
||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_parents_keeps_teams(team, project):
|
||||
project.update_role.parents.add(team.member_role)
|
||||
assert team.member_role in project.update_role # test prep sanity check
|
||||
update_role_parentage_for_instance(project)
|
||||
assert team.member_role in project.update_role # actual assertion
|
||||
|
||||
@ -102,21 +102,21 @@ class TestOAuth2Application:
|
||||
assert access.can_delete(app) is can_access
|
||||
|
||||
|
||||
def test_superuser_can_always_create(self, admin, org_admin, org_member, alice):
|
||||
def test_superuser_can_always_create(self, admin, org_admin, org_member, alice, organization):
|
||||
access = OAuth2ApplicationAccess(admin)
|
||||
for user in [admin, org_admin, org_member, alice]:
|
||||
assert access.can_add({
|
||||
'name': 'test app', 'user': user.pk, 'client_type': 'confidential',
|
||||
'authorization_grant_type': 'password', 'organization': 1
|
||||
'authorization_grant_type': 'password', 'organization': organization.id
|
||||
})
|
||||
|
||||
def test_normal_user_cannot_create(self, admin, org_admin, org_member, alice):
|
||||
def test_normal_user_cannot_create(self, admin, org_admin, org_member, alice, organization):
|
||||
for access_user in [org_member, alice]:
|
||||
access = OAuth2ApplicationAccess(access_user)
|
||||
for user in [admin, org_admin, org_member, alice]:
|
||||
assert not access.can_add({
|
||||
'name': 'test app', 'user': user.pk, 'client_type': 'confidential',
|
||||
'authorization_grant_type': 'password', 'organization': 1
|
||||
'authorization_grant_type': 'password', 'organization': organization.id
|
||||
})
|
||||
|
||||
|
||||
|
||||
6
awx/main/tests/test_env.py
Normal file
6
awx/main/tests/test_env.py
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
# Ensure that our autouse overwrites are working
|
||||
def test_cache(settings):
|
||||
assert settings.CACHES['default']['BACKEND'] == 'django.core.cache.backends.locmem.LocMemCache'
|
||||
assert settings.CACHES['default']['LOCATION'].startswith('unique-')
|
||||
@ -3,14 +3,19 @@
|
||||
import pytest
|
||||
|
||||
from rest_framework.exceptions import PermissionDenied, ParseError
|
||||
from awx.api.filters import FieldLookupBackend
|
||||
from awx.api.filters import FieldLookupBackend, OrderByBackend, get_field_from_path
|
||||
from awx.main.models import (AdHocCommand, ActivityStream,
|
||||
CustomInventoryScript, Credential, Job,
|
||||
JobTemplate, SystemJob, UnifiedJob, User,
|
||||
WorkflowJob, WorkflowJobTemplate,
|
||||
WorkflowJobOptions, InventorySource)
|
||||
WorkflowJobOptions, InventorySource,
|
||||
JobEvent)
|
||||
from awx.main.models.oauth import OAuth2Application
|
||||
from awx.main.models.jobs import JobOptions
|
||||
|
||||
# Django
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
|
||||
|
||||
def test_related():
|
||||
field_lookup = FieldLookupBackend()
|
||||
@ -20,6 +25,27 @@ def test_related():
|
||||
print(new_lookup)
|
||||
|
||||
|
||||
def test_invalid_filter_key():
|
||||
field_lookup = FieldLookupBackend()
|
||||
# FieldDoesNotExist is caught and converted to ParseError by filter_queryset
|
||||
with pytest.raises(FieldDoesNotExist) as excinfo:
|
||||
field_lookup.value_to_python(JobEvent, 'event_data.task_action', 'foo')
|
||||
assert 'has no field named' in str(excinfo)
|
||||
|
||||
|
||||
def test_invalid_field_hop():
|
||||
with pytest.raises(ParseError) as excinfo:
|
||||
get_field_from_path(Credential, 'organization__description__user')
|
||||
assert 'No related model for' in str(excinfo)
|
||||
|
||||
|
||||
def test_invalid_order_by_key():
|
||||
field_order_by = OrderByBackend()
|
||||
with pytest.raises(ParseError) as excinfo:
|
||||
[f for f in field_order_by._validate_ordering_fields(JobEvent, ('event_data.task_action',))]
|
||||
assert 'has no field named' in str(excinfo)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(u"empty_value", [u'', ''])
|
||||
def test_empty_in(empty_value):
|
||||
field_lookup = FieldLookupBackend()
|
||||
@ -57,7 +83,6 @@ def test_filter_on_password_field(password_field, lookup_suffix):
|
||||
(User, 'password__icontains'),
|
||||
(User, 'settings__value__icontains'),
|
||||
(User, 'main_oauth2accesstoken__token__gt'),
|
||||
(User, 'main_oauth2application__name__gt'),
|
||||
(UnifiedJob, 'job_args__icontains'),
|
||||
(UnifiedJob, 'job_env__icontains'),
|
||||
(UnifiedJob, 'start_args__icontains'),
|
||||
@ -70,8 +95,8 @@ def test_filter_on_password_field(password_field, lookup_suffix):
|
||||
(JobTemplate, 'survey_spec__icontains'),
|
||||
(WorkflowJobTemplate, 'survey_spec__icontains'),
|
||||
(CustomInventoryScript, 'script__icontains'),
|
||||
(ActivityStream, 'o_auth2_access_token__gt'),
|
||||
(ActivityStream, 'o_auth2_application__gt')
|
||||
(ActivityStream, 'o_auth2_application__client_secret__gt'),
|
||||
(OAuth2Application, 'grant__code__gt')
|
||||
])
|
||||
def test_filter_sensitive_fields_and_relations(model, query):
|
||||
field_lookup = FieldLookupBackend()
|
||||
|
||||
@ -16,6 +16,9 @@ from awx.api.views import (
|
||||
from awx.main.models import (
|
||||
Host,
|
||||
)
|
||||
from awx.main.views import handle_error
|
||||
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -25,6 +28,12 @@ def mock_response_new(mocker):
|
||||
return m
|
||||
|
||||
|
||||
def test_handle_error():
|
||||
# Assure that templating of error does not raise errors
|
||||
request = APIRequestFactory().get('/fooooo/')
|
||||
handle_error(request)
|
||||
|
||||
|
||||
class TestApiRootView:
|
||||
def test_get_endpoints(self, mocker, mock_response_new):
|
||||
endpoints = [
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
import logging
|
||||
|
||||
from mock import PropertyMock
|
||||
|
||||
@ -7,3 +8,16 @@ from mock import PropertyMock
|
||||
def _disable_database_settings(mocker):
|
||||
m = mocker.patch('awx.conf.settings.SettingsWrapper.all_supported_settings', new_callable=PropertyMock)
|
||||
m.return_value = []
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def dummy_log_record():
|
||||
return logging.LogRecord(
|
||||
'awx', # logger name
|
||||
20, # loglevel INFO
|
||||
'./awx/some/module.py', # pathname
|
||||
100, # lineno
|
||||
'User joe logged in', # msg
|
||||
tuple(), # args,
|
||||
None # exc_info
|
||||
)
|
||||
|
||||
@ -90,7 +90,7 @@ def test_cancel_callback_error():
|
||||
|
||||
extra_fields = {}
|
||||
status, rc = run.run_pexpect(
|
||||
['ls', '-la'],
|
||||
['sleep', '2'],
|
||||
HERE,
|
||||
{},
|
||||
stdout,
|
||||
|
||||
@ -44,3 +44,18 @@ def test_playbook_event_strip_invalid_keys(job_identifier, cls):
|
||||
'extra_key': 'extra_value'
|
||||
})
|
||||
manager.create.assert_called_with(**{job_identifier: 123})
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field', [
|
||||
'play', 'role', 'task', 'playbook'
|
||||
])
|
||||
def test_really_long_event_fields(field):
|
||||
with mock.patch.object(JobEvent, 'objects') as manager:
|
||||
JobEvent.create_from_data(**{
|
||||
'job_id': 123,
|
||||
field: 'X' * 4096
|
||||
})
|
||||
manager.create.assert_called_with(**{
|
||||
'job_id': 123,
|
||||
field: 'X' * 1021 + '...'
|
||||
})
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import tempfile
|
||||
import json
|
||||
import yaml
|
||||
|
||||
import pytest
|
||||
from itertools import count
|
||||
|
||||
from awx.main.utils.encryption import encrypt_value
|
||||
from awx.main.tasks import RunJob
|
||||
from awx.main.models import (
|
||||
@ -16,6 +17,15 @@ from awx.main.utils.safe_yaml import SafeLoader
|
||||
ENCRYPTED_SECRET = encrypt_value('secret')
|
||||
|
||||
|
||||
class DistinctParametrize(object):
|
||||
|
||||
def __init__(self):
|
||||
self._gen = count(0)
|
||||
|
||||
def __call__(self, value):
|
||||
return str(next(self._gen))
|
||||
|
||||
|
||||
@pytest.mark.survey
|
||||
class SurveyVariableValidation:
|
||||
|
||||
@ -243,7 +253,7 @@ def test_optional_survey_question_defaults(
|
||||
('password', 'foo', 5, {'extra_vars': {'x': ''}}, {'x': ''}),
|
||||
('password', ENCRYPTED_SECRET, 5, {'extra_vars': {'x': '$encrypted$'}}, {}),
|
||||
('password', ENCRYPTED_SECRET, 10, {'extra_vars': {'x': '$encrypted$'}}, {'x': ENCRYPTED_SECRET}),
|
||||
])
|
||||
], ids=DistinctParametrize())
|
||||
def test_survey_encryption_defaults(survey_spec_factory, question_type, default, maxlen, kwargs, expected):
|
||||
spec = survey_spec_factory([
|
||||
{
|
||||
|
||||
@ -158,7 +158,7 @@ def test_jt_existing_values_are_nonsensitive(job_template_with_ids, user_unit):
|
||||
"""Assure that permission checks are not required if submitted data is
|
||||
identical to what the job template already has."""
|
||||
|
||||
data = model_to_dict(job_template_with_ids)
|
||||
data = model_to_dict(job_template_with_ids, exclude=['unifiedjobtemplate_ptr'])
|
||||
access = JobTemplateAccess(user_unit)
|
||||
|
||||
assert access.changes_are_non_sensitive(job_template_with_ids, data)
|
||||
|
||||
@ -96,10 +96,26 @@ def test_cred_type_input_schema_validity(input_, valid):
|
||||
({'invalid-injector': {}}, False),
|
||||
({'file': 123}, False),
|
||||
({'file': {}}, True),
|
||||
# Uses credential inputs inside of unnamed file contents
|
||||
({'file': {'template': '{{username}}'}}, True),
|
||||
# Uses named file
|
||||
({'file': {'template.username': '{{username}}'}}, True),
|
||||
# Uses multiple named files
|
||||
({'file': {'template.username': '{{username}}', 'template.password': '{{pass}}'}}, True),
|
||||
# Use of unnamed file mutually exclusive with use of named files
|
||||
({'file': {'template': '{{username}}', 'template.password': '{{pass}}'}}, False),
|
||||
# References non-existant named file
|
||||
({'env': {'FROM_FILE': "{{tower.filename.cert}}"}}, False),
|
||||
# References unnamed file, but a file was never defined
|
||||
({'env': {'FROM_FILE': "{{tower.filename}}"}}, False),
|
||||
# Cannot reference tower namespace itself (what would this return??)
|
||||
({'env': {'FROM_FILE': "{{tower}}"}}, False),
|
||||
# References filename of a named file
|
||||
({'file': {'template.cert': '{{awx_secret}}'}, 'env': {'FROM_FILE': "{{tower.filename.cert}}"}}, True),
|
||||
# With named files, `tower.filename` is another namespace, so it cannot be referenced
|
||||
({'file': {'template.cert': '{{awx_secret}}'}, 'env': {'FROM_FILE': "{{tower.filename}}"}}, False),
|
||||
# With an unnamed file, `tower.filename` is just the filename
|
||||
({'file': {'template': '{{awx_secret}}'}, 'env': {'THE_FILENAME': "{{tower.filename}}"}}, True),
|
||||
({'file': {'foo': 'bar'}}, False),
|
||||
({'env': 123}, False),
|
||||
({'env': {}}, True),
|
||||
|
||||
@ -2155,7 +2155,7 @@ def test_aquire_lock_open_fail_logged(logging_getLogger, os_open):
|
||||
|
||||
ProjectUpdate = tasks.RunProjectUpdate()
|
||||
|
||||
with pytest.raises(OSError, errno=3, strerror='dummy message'):
|
||||
with pytest.raises(OSError, message='dummy message'):
|
||||
ProjectUpdate.acquire_lock(instance)
|
||||
assert logger.err.called_with("I/O error({0}) while trying to open lock file [{1}]: {2}".format(3, 'this_file_does_not_exist', 'dummy message'))
|
||||
|
||||
@ -2181,7 +2181,7 @@ def test_aquire_lock_acquisition_fail_logged(fcntl_flock, logging_getLogger, os_
|
||||
|
||||
ProjectUpdate = tasks.RunProjectUpdate()
|
||||
|
||||
with pytest.raises(IOError, errno=3, strerror='dummy message'):
|
||||
with pytest.raises(IOError, message='dummy message'):
|
||||
ProjectUpdate.acquire_lock(instance)
|
||||
os_close.assert_called_with(3)
|
||||
assert logger.err.called_with("I/O error({0}) while trying to aquire lock on file [{1}]: {2}".format(3, 'this_file_does_not_exist', 'dummy message'))
|
||||
|
||||
@ -3,6 +3,10 @@ import mock
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.generics import ListAPIView
|
||||
|
||||
# Django
|
||||
from django.core.urlresolvers import RegexURLResolver, RegexURLPattern
|
||||
|
||||
# AWX
|
||||
from awx.main.views import ApiErrorView
|
||||
@ -52,3 +56,44 @@ def test_disable_post_on_v1_inventory_source_list(version, supports_post):
|
||||
inv_source_list.request = mock.MagicMock()
|
||||
with mock.patch('awx.api.views.get_request_version', return_value=version):
|
||||
assert ('POST' in inv_source_list.allowed_methods) == supports_post
|
||||
|
||||
|
||||
def test_views_have_search_fields():
|
||||
from awx.api.urls import urlpatterns as api_patterns
|
||||
patterns = set([])
|
||||
url_views = set([])
|
||||
# Add recursive URL patterns
|
||||
unprocessed = set(api_patterns)
|
||||
while unprocessed:
|
||||
to_process = unprocessed.copy()
|
||||
unprocessed = set([])
|
||||
for pattern in to_process:
|
||||
if hasattr(pattern, 'lookup_str') and not pattern.lookup_str.startswith('awx.api'):
|
||||
continue
|
||||
patterns.add(pattern)
|
||||
if isinstance(pattern, RegexURLResolver):
|
||||
for sub_pattern in pattern.url_patterns:
|
||||
if sub_pattern not in patterns:
|
||||
unprocessed.add(sub_pattern)
|
||||
# Get view classes
|
||||
for pattern in patterns:
|
||||
if isinstance(pattern, RegexURLPattern) and hasattr(pattern.callback, 'view_class'):
|
||||
cls = pattern.callback.view_class
|
||||
if issubclass(cls, ListAPIView):
|
||||
url_views.add(pattern.callback.view_class)
|
||||
|
||||
# Gather any views that don't have search fields defined
|
||||
views_missing_search = []
|
||||
for View in url_views:
|
||||
view = View()
|
||||
if not hasattr(view, 'search_fields') or len(view.search_fields) == 0:
|
||||
views_missing_search.append(view)
|
||||
|
||||
if views_missing_search:
|
||||
raise Exception('{} views do not have search fields defined:\n{}'.format(
|
||||
len(views_missing_search),
|
||||
'\n'.join([
|
||||
v.__class__.__name__ + ' (model: {})'.format(getattr(v, 'model', type(None)).__name__)
|
||||
for v in views_missing_search
|
||||
]))
|
||||
)
|
||||
|
||||
@ -7,10 +7,10 @@ import pytest
|
||||
from uuid import uuid4
|
||||
import json
|
||||
import yaml
|
||||
import mock
|
||||
|
||||
from backports.tempfile import TemporaryDirectory
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
||||
from rest_framework.exceptions import ParseError
|
||||
|
||||
@ -26,14 +26,6 @@ from awx.main.models import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_cache():
|
||||
'''
|
||||
Clear cache (local memory) for each test to prevent using cached settings.
|
||||
'''
|
||||
cache.clear()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('input_, output', [
|
||||
({"foo": "bar"}, {"foo": "bar"}),
|
||||
('{"foo": "bar"}', {"foo": "bar"}),
|
||||
@ -114,46 +106,48 @@ def test_get_type_for_model(model, name):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def memoized_function(mocker):
|
||||
@common.memoize(track_function=True)
|
||||
def myfunction(key, value):
|
||||
if key not in myfunction.calls:
|
||||
myfunction.calls[key] = 0
|
||||
def memoized_function(mocker, mock_cache):
|
||||
with mock.patch('awx.main.utils.common.get_memoize_cache', return_value=mock_cache):
|
||||
@common.memoize(track_function=True)
|
||||
def myfunction(key, value):
|
||||
if key not in myfunction.calls:
|
||||
myfunction.calls[key] = 0
|
||||
|
||||
myfunction.calls[key] += 1
|
||||
myfunction.calls[key] += 1
|
||||
|
||||
if myfunction.calls[key] == 1:
|
||||
return value
|
||||
else:
|
||||
return '%s called %s times' % (value, myfunction.calls[key])
|
||||
myfunction.calls = dict()
|
||||
return myfunction
|
||||
if myfunction.calls[key] == 1:
|
||||
return value
|
||||
else:
|
||||
return '%s called %s times' % (value, myfunction.calls[key])
|
||||
myfunction.calls = dict()
|
||||
return myfunction
|
||||
|
||||
|
||||
def test_memoize_track_function(memoized_function):
|
||||
def test_memoize_track_function(memoized_function, mock_cache):
|
||||
assert memoized_function('scott', 'scotterson') == 'scotterson'
|
||||
assert cache.get('myfunction') == {u'scott-scotterson': 'scotterson'}
|
||||
assert mock_cache.get('myfunction') == {u'scott-scotterson': 'scotterson'}
|
||||
assert memoized_function('scott', 'scotterson') == 'scotterson'
|
||||
|
||||
assert memoized_function.calls['scott'] == 1
|
||||
|
||||
assert memoized_function('john', 'smith') == 'smith'
|
||||
assert cache.get('myfunction') == {u'scott-scotterson': 'scotterson', u'john-smith': 'smith'}
|
||||
assert mock_cache.get('myfunction') == {u'scott-scotterson': 'scotterson', u'john-smith': 'smith'}
|
||||
assert memoized_function('john', 'smith') == 'smith'
|
||||
|
||||
|
||||
assert memoized_function.calls['john'] == 1
|
||||
|
||||
|
||||
def test_memoize_delete(memoized_function):
|
||||
def test_memoize_delete(memoized_function, mock_cache):
|
||||
assert memoized_function('john', 'smith') == 'smith'
|
||||
assert memoized_function('john', 'smith') == 'smith'
|
||||
assert memoized_function.calls['john'] == 1
|
||||
|
||||
assert cache.get('myfunction') == {u'john-smith': 'smith'}
|
||||
assert mock_cache.get('myfunction') == {u'john-smith': 'smith'}
|
||||
|
||||
common.memoize_delete('myfunction')
|
||||
with mock.patch('awx.main.utils.common.memoize_delete', side_effect=mock_cache.delete):
|
||||
common.memoize_delete('myfunction')
|
||||
|
||||
assert cache.get('myfunction') is None
|
||||
assert mock_cache.get('myfunction') is None
|
||||
|
||||
assert memoized_function('john', 'smith') == 'smith called 2 times'
|
||||
assert memoized_function.calls['john'] == 2
|
||||
|
||||
@ -5,7 +5,7 @@ import mock
|
||||
from collections import namedtuple
|
||||
|
||||
# AWX
|
||||
from awx.main.utils.filters import SmartFilter
|
||||
from awx.main.utils.filters import SmartFilter, ExternalLoggerEnabled
|
||||
|
||||
# Django
|
||||
from django.db.models import Q
|
||||
@ -13,6 +13,37 @@ from django.db.models import Q
|
||||
import six
|
||||
|
||||
|
||||
@pytest.mark.parametrize('params, logger_name, expected', [
|
||||
# skip all records if enabled_flag = False
|
||||
({'enabled_flag': False}, 'awx.main', False),
|
||||
# skip all records if the host is undefined
|
||||
({'enabled_flag': True}, 'awx.main', False),
|
||||
# skip all records if underlying logger is used by handlers themselves
|
||||
({'enabled_flag': True}, 'awx.main.utils.handlers', False),
|
||||
({'enabled_flag': True, 'enabled_loggers': ['awx']}, 'awx.main', True),
|
||||
({'enabled_flag': True, 'enabled_loggers': ['abc']}, 'awx.analytics.xyz', False),
|
||||
({'enabled_flag': True, 'enabled_loggers': ['xyz']}, 'awx.analytics.xyz', True),
|
||||
])
|
||||
def test_base_logging_handler_skip_log(params, logger_name, expected, dummy_log_record):
|
||||
filter = ExternalLoggerEnabled(**params)
|
||||
dummy_log_record.name = logger_name
|
||||
assert filter.filter(dummy_log_record) is expected, (params, logger_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('level, expect', [
|
||||
(30, True), # warning
|
||||
(20, False) # info
|
||||
])
|
||||
def test_log_configurable_severity(level, expect, dummy_log_record):
|
||||
dummy_log_record.levelno = level
|
||||
filter = ExternalLoggerEnabled(
|
||||
enabled_flag=True,
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'],
|
||||
lvl='WARNING'
|
||||
)
|
||||
assert filter.filter(dummy_log_record) is expect
|
||||
|
||||
|
||||
Field = namedtuple('Field', 'name')
|
||||
Meta = namedtuple('Meta', 'fields')
|
||||
|
||||
|
||||
@ -6,22 +6,15 @@
|
||||
# python
|
||||
import pytest
|
||||
import mock
|
||||
from contextlib import nested
|
||||
|
||||
# AWX
|
||||
from awx.main.utils.ha import (
|
||||
_add_remove_celery_worker_queues,
|
||||
update_celery_worker_routes,
|
||||
AWXCeleryRouter,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conf():
|
||||
class Conf():
|
||||
CELERY_ROUTES = dict()
|
||||
CELERYBEAT_SCHEDULE = dict()
|
||||
return Conf()
|
||||
|
||||
|
||||
class TestAddRemoveCeleryWorkerQueues():
|
||||
@pytest.fixture
|
||||
def instance_generator(self, mocker):
|
||||
@ -47,54 +40,54 @@ class TestAddRemoveCeleryWorkerQueues():
|
||||
app.control.cancel_consumer = mocker.MagicMock()
|
||||
return app
|
||||
|
||||
@pytest.mark.parametrize("static_queues,_worker_queues,groups,hostname,added_expected,removed_expected", [
|
||||
(['east', 'west'], ['east', 'west', 'east-1'], [], 'east-1', [], []),
|
||||
([], ['east', 'west', 'east-1'], ['east', 'west'], 'east-1', [], []),
|
||||
([], ['east', 'west'], ['east', 'west'], 'east-1', ['east-1'], []),
|
||||
([], [], ['east', 'west'], 'east-1', ['east', 'west', 'east-1'], []),
|
||||
([], ['china', 'russia'], ['east', 'west'], 'east-1', ['east', 'west', 'east-1'], ['china', 'russia']),
|
||||
@pytest.mark.parametrize("broadcast_queues,static_queues,_worker_queues,groups,hostname,added_expected,removed_expected", [
|
||||
(['tower_broadcast_all'], ['east', 'west'], ['east', 'west', 'east-1'], [], 'east-1', ['tower_broadcast_all_east-1'], []),
|
||||
([], [], ['east', 'west', 'east-1'], ['east', 'west'], 'east-1', [], []),
|
||||
([], [], ['east', 'west'], ['east', 'west'], 'east-1', ['east-1'], []),
|
||||
([], [], [], ['east', 'west'], 'east-1', ['east', 'west', 'east-1'], []),
|
||||
([], [], ['china', 'russia'], ['east', 'west'], 'east-1', ['east', 'west', 'east-1'], ['china', 'russia']),
|
||||
])
|
||||
def test__add_remove_celery_worker_queues_noop(self, mock_app,
|
||||
instance_generator,
|
||||
worker_queues_generator,
|
||||
static_queues, _worker_queues,
|
||||
instance_generator,
|
||||
worker_queues_generator,
|
||||
broadcast_queues,
|
||||
static_queues, _worker_queues,
|
||||
groups, hostname,
|
||||
added_expected, removed_expected):
|
||||
instance = instance_generator(groups=groups, hostname=hostname)
|
||||
worker_queues = worker_queues_generator(_worker_queues)
|
||||
with mock.patch('awx.main.utils.ha.settings.AWX_CELERY_QUEUES_STATIC', static_queues):
|
||||
with nested(
|
||||
mock.patch('awx.main.utils.ha.settings.AWX_CELERY_QUEUES_STATIC', static_queues),
|
||||
mock.patch('awx.main.utils.ha.settings.AWX_CELERY_BCAST_QUEUES_STATIC', broadcast_queues),
|
||||
mock.patch('awx.main.utils.ha.settings.CLUSTER_HOST_ID', hostname)):
|
||||
(added_queues, removed_queues) = _add_remove_celery_worker_queues(mock_app, [instance], worker_queues, hostname)
|
||||
assert set(added_queues) == set(added_expected)
|
||||
assert set(removed_queues) == set(removed_expected)
|
||||
|
||||
|
||||
class TestUpdateCeleryWorkerRoutes():
|
||||
class TestUpdateCeleryWorkerRouter():
|
||||
|
||||
@pytest.mark.parametrize("is_controller,expected_routes", [
|
||||
(False, {
|
||||
(False, {
|
||||
'awx.main.tasks.cluster_node_heartbeat': {'queue': 'east-1', 'routing_key': 'east-1'},
|
||||
'awx.main.tasks.purge_old_stdout_files': {'queue': 'east-1', 'routing_key': 'east-1'}
|
||||
}),
|
||||
(True, {
|
||||
(True, {
|
||||
'awx.main.tasks.cluster_node_heartbeat': {'queue': 'east-1', 'routing_key': 'east-1'},
|
||||
'awx.main.tasks.purge_old_stdout_files': {'queue': 'east-1', 'routing_key': 'east-1'},
|
||||
'awx.main.tasks.awx_isolated_heartbeat': {'queue': 'east-1', 'routing_key': 'east-1'},
|
||||
}),
|
||||
])
|
||||
def test_update_celery_worker_routes(self, mocker, conf, is_controller, expected_routes):
|
||||
instance = mocker.MagicMock()
|
||||
instance.hostname = 'east-1'
|
||||
instance.is_controller = mocker.MagicMock(return_value=is_controller)
|
||||
def test_update_celery_worker_routes(self, mocker, is_controller, expected_routes):
|
||||
def get_or_register():
|
||||
instance = mock.MagicMock()
|
||||
instance.hostname = 'east-1'
|
||||
instance.is_controller = mock.MagicMock(return_value=is_controller)
|
||||
return (False, instance)
|
||||
|
||||
assert update_celery_worker_routes(instance, conf) == expected_routes
|
||||
assert conf.CELERY_ROUTES == expected_routes
|
||||
with mock.patch('awx.main.models.Instance.objects.get_or_register', get_or_register):
|
||||
router = AWXCeleryRouter()
|
||||
|
||||
def test_update_celery_worker_routes_deleted(self, mocker, conf):
|
||||
instance = mocker.MagicMock()
|
||||
instance.hostname = 'east-1'
|
||||
instance.is_controller = mocker.MagicMock(return_value=False)
|
||||
conf.CELERY_ROUTES = {'awx.main.tasks.awx_isolated_heartbeat': 'foobar'}
|
||||
|
||||
update_celery_worker_routes(instance, conf)
|
||||
assert 'awx.main.tasks.awx_isolated_heartbeat' not in conf.CELERY_ROUTES
|
||||
for k,v in expected_routes.iteritems():
|
||||
assert router.route_for_task(k) == v
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import base64
|
||||
import cStringIO
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
import datetime
|
||||
@ -10,7 +9,6 @@ from uuid import uuid4
|
||||
|
||||
import mock
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf import LazySettings
|
||||
import pytest
|
||||
import requests
|
||||
@ -18,23 +16,11 @@ from requests_futures.sessions import FuturesSession
|
||||
|
||||
from awx.main.utils.handlers import (BaseHandler, BaseHTTPSHandler as HTTPSHandler,
|
||||
TCPHandler, UDPHandler, _encode_payload_for_socket,
|
||||
PARAM_NAMES, LoggingConnectivityException)
|
||||
PARAM_NAMES, LoggingConnectivityException,
|
||||
AWXProxyHandler)
|
||||
from awx.main.utils.formatters import LogstashFormatter
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def dummy_log_record():
|
||||
return logging.LogRecord(
|
||||
'awx', # logger name
|
||||
20, # loglevel INFO
|
||||
'./awx/some/module.py', # pathname
|
||||
100, # lineno
|
||||
'User joe logged in', # msg
|
||||
tuple(), # args,
|
||||
None # exc_info
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def http_adapter():
|
||||
class FakeHTTPAdapter(requests.adapters.HTTPAdapter):
|
||||
@ -80,105 +66,91 @@ def test_https_logging_handler_requests_async_implementation():
|
||||
|
||||
|
||||
def test_https_logging_handler_has_default_http_timeout():
|
||||
handler = HTTPSHandler.from_django_settings(settings)
|
||||
handler = TCPHandler()
|
||||
assert handler.tcp_timeout == 5
|
||||
|
||||
|
||||
@pytest.mark.parametrize('param', PARAM_NAMES.keys())
|
||||
@pytest.mark.parametrize('param', ['host', 'port', 'indv_facts'])
|
||||
def test_base_logging_handler_defaults(param):
|
||||
handler = BaseHandler()
|
||||
assert hasattr(handler, param) and getattr(handler, param) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('param', PARAM_NAMES.keys())
|
||||
@pytest.mark.parametrize('param', ['host', 'port', 'indv_facts'])
|
||||
def test_base_logging_handler_kwargs(param):
|
||||
handler = BaseHandler(**{param: 'EXAMPLE'})
|
||||
assert hasattr(handler, param) and getattr(handler, param) == 'EXAMPLE'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('param, django_settings_name', PARAM_NAMES.items())
|
||||
def test_base_logging_handler_from_django_settings(param, django_settings_name):
|
||||
@pytest.mark.parametrize('params', [
|
||||
{
|
||||
'LOG_AGGREGATOR_HOST': 'https://server.invalid',
|
||||
'LOG_AGGREGATOR_PORT': 22222,
|
||||
'LOG_AGGREGATOR_TYPE': 'loggly',
|
||||
'LOG_AGGREGATOR_USERNAME': 'foo',
|
||||
'LOG_AGGREGATOR_PASSWORD': 'bar',
|
||||
'LOG_AGGREGATOR_INDIVIDUAL_FACTS': True,
|
||||
'LOG_AGGREGATOR_TCP_TIMEOUT': 96,
|
||||
'LOG_AGGREGATOR_VERIFY_CERT': False,
|
||||
'LOG_AGGREGATOR_PROTOCOL': 'https'
|
||||
},
|
||||
{
|
||||
'LOG_AGGREGATOR_HOST': 'https://server.invalid',
|
||||
'LOG_AGGREGATOR_PORT': 22222,
|
||||
'LOG_AGGREGATOR_PROTOCOL': 'udp'
|
||||
}
|
||||
])
|
||||
def test_real_handler_from_django_settings(params):
|
||||
settings = LazySettings()
|
||||
settings.configure(**params)
|
||||
handler = AWXProxyHandler().get_handler(custom_settings=settings)
|
||||
# need the _reverse_ dictionary from PARAM_NAMES
|
||||
attr_lookup = {}
|
||||
for attr_name, setting_name in PARAM_NAMES.items():
|
||||
attr_lookup[setting_name] = attr_name
|
||||
for setting_name, val in params.items():
|
||||
attr_name = attr_lookup[setting_name]
|
||||
if attr_name == 'protocol':
|
||||
continue
|
||||
assert hasattr(handler, attr_name)
|
||||
|
||||
|
||||
def test_invalid_kwarg_to_real_handler():
|
||||
settings = LazySettings()
|
||||
settings.configure(**{
|
||||
django_settings_name: 'EXAMPLE'
|
||||
'LOG_AGGREGATOR_HOST': 'https://server.invalid',
|
||||
'LOG_AGGREGATOR_PORT': 22222,
|
||||
'LOG_AGGREGATOR_PROTOCOL': 'udp',
|
||||
'LOG_AGGREGATOR_VERIFY_CERT': False # setting not valid for UDP handler
|
||||
})
|
||||
handler = BaseHandler.from_django_settings(settings)
|
||||
assert hasattr(handler, param) and getattr(handler, param) == 'EXAMPLE'
|
||||
handler = AWXProxyHandler().get_handler(custom_settings=settings)
|
||||
assert not hasattr(handler, 'verify_cert')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('params, logger_name, expected', [
|
||||
# skip all records if enabled_flag = False
|
||||
({'enabled_flag': False}, 'awx.main', True),
|
||||
# skip all records if the host is undefined
|
||||
({'host': '', 'enabled_flag': True}, 'awx.main', True),
|
||||
# skip all records if underlying logger is used by handlers themselves
|
||||
({'host': '127.0.0.1', 'enabled_flag': True}, 'awx.main.utils.handlers', True),
|
||||
({'host': '127.0.0.1', 'enabled_flag': True}, 'awx.main', False),
|
||||
({'host': '127.0.0.1', 'enabled_flag': True, 'enabled_loggers': ['abc']}, 'awx.analytics.xyz', True),
|
||||
({'host': '127.0.0.1', 'enabled_flag': True, 'enabled_loggers': ['xyz']}, 'awx.analytics.xyz', False),
|
||||
])
|
||||
def test_base_logging_handler_skip_log(params, logger_name, expected):
|
||||
handler = BaseHandler(**params)
|
||||
assert handler._skip_log(logger_name) is expected
|
||||
|
||||
|
||||
def test_base_logging_handler_emit(dummy_log_record):
|
||||
handler = BaseHandler(host='127.0.0.1', enabled_flag=True,
|
||||
message_type='logstash', lvl='INFO',
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
def test_base_logging_handler_emit_system_tracking(dummy_log_record):
|
||||
handler = BaseHandler(host='127.0.0.1', indv_facts=True)
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
sent_payloads = handler.emit(dummy_log_record)
|
||||
|
||||
assert len(sent_payloads) == 1
|
||||
body = json.loads(sent_payloads[0])
|
||||
|
||||
assert body['level'] == 'INFO'
|
||||
assert body['logger_name'] == 'awx'
|
||||
assert body['message'] == 'User joe logged in'
|
||||
|
||||
|
||||
def test_base_logging_handler_ignore_low_severity_msg(dummy_log_record):
|
||||
handler = BaseHandler(host='127.0.0.1', enabled_flag=True,
|
||||
message_type='logstash', lvl='WARNING',
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
sent_payloads = handler.emit(dummy_log_record)
|
||||
assert len(sent_payloads) == 0
|
||||
|
||||
|
||||
def test_base_logging_handler_emit_system_tracking():
|
||||
handler = BaseHandler(host='127.0.0.1', enabled_flag=True,
|
||||
message_type='logstash', indv_facts=True, lvl='INFO',
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
record = logging.LogRecord(
|
||||
'awx.analytics.system_tracking', # logger name
|
||||
20, # loglevel INFO
|
||||
'./awx/some/module.py', # pathname
|
||||
100, # lineno
|
||||
None, # msg
|
||||
tuple(), # args,
|
||||
None # exc_info
|
||||
)
|
||||
record.inventory_id = 11
|
||||
record.host_name = 'my_lucky_host'
|
||||
record.job_id = 777
|
||||
record.ansible_facts = {
|
||||
dummy_log_record.name = 'awx.analytics.system_tracking'
|
||||
dummy_log_record.msg = None
|
||||
dummy_log_record.inventory_id = 11
|
||||
dummy_log_record.host_name = 'my_lucky_host'
|
||||
dummy_log_record.job_id = 777
|
||||
dummy_log_record.ansible_facts = {
|
||||
"ansible_kernel": "4.4.66-boot2docker",
|
||||
"ansible_machine": "x86_64",
|
||||
"ansible_swapfree_mb": 4663,
|
||||
}
|
||||
record.ansible_facts_modified = datetime.datetime.now(tzutc()).isoformat()
|
||||
sent_payloads = handler.emit(record)
|
||||
dummy_log_record.ansible_facts_modified = datetime.datetime.now(tzutc()).isoformat()
|
||||
sent_payloads = handler.emit(dummy_log_record)
|
||||
|
||||
assert len(sent_payloads) == 1
|
||||
assert sent_payloads[0]['ansible_facts'] == record.ansible_facts
|
||||
assert sent_payloads[0]['ansible_facts_modified'] == record.ansible_facts_modified
|
||||
assert sent_payloads[0]['ansible_facts'] == dummy_log_record.ansible_facts
|
||||
assert sent_payloads[0]['ansible_facts_modified'] == dummy_log_record.ansible_facts_modified
|
||||
assert sent_payloads[0]['level'] == 'INFO'
|
||||
assert sent_payloads[0]['logger_name'] == 'awx.analytics.system_tracking'
|
||||
assert sent_payloads[0]['job_id'] == record.job_id
|
||||
assert sent_payloads[0]['inventory_id'] == record.inventory_id
|
||||
assert sent_payloads[0]['host_name'] == record.host_name
|
||||
assert sent_payloads[0]['job_id'] == dummy_log_record.job_id
|
||||
assert sent_payloads[0]['inventory_id'] == dummy_log_record.inventory_id
|
||||
assert sent_payloads[0]['host_name'] == dummy_log_record.host_name
|
||||
|
||||
|
||||
@pytest.mark.parametrize('host, port, normalized, hostname_only', [
|
||||
@ -236,16 +208,18 @@ def test_https_logging_handler_connectivity_test(http_adapter, status, reason, e
|
||||
def emit(self, record):
|
||||
return super(FakeHTTPSHandler, self).emit(record)
|
||||
|
||||
if exc:
|
||||
with pytest.raises(exc) as e:
|
||||
FakeHTTPSHandler.perform_test(settings)
|
||||
assert str(e).endswith('%s: %s' % (status, reason))
|
||||
else:
|
||||
assert FakeHTTPSHandler.perform_test(settings) is None
|
||||
with mock.patch.object(AWXProxyHandler, 'get_handler_class') as mock_get_class:
|
||||
mock_get_class.return_value = FakeHTTPSHandler
|
||||
if exc:
|
||||
with pytest.raises(exc) as e:
|
||||
AWXProxyHandler().perform_test(settings)
|
||||
assert str(e).endswith('%s: %s' % (status, reason))
|
||||
else:
|
||||
assert AWXProxyHandler().perform_test(settings) is None
|
||||
|
||||
|
||||
def test_https_logging_handler_logstash_auth_info():
|
||||
handler = HTTPSHandler(message_type='logstash', username='bob', password='ansible', lvl='INFO')
|
||||
handler = HTTPSHandler(message_type='logstash', username='bob', password='ansible')
|
||||
handler._add_auth_information()
|
||||
assert isinstance(handler.session.auth, requests.auth.HTTPBasicAuth)
|
||||
assert handler.session.auth.username == 'bob'
|
||||
@ -261,9 +235,7 @@ def test_https_logging_handler_splunk_auth_info():
|
||||
|
||||
def test_https_logging_handler_connection_error(connection_error_adapter,
|
||||
dummy_log_record):
|
||||
handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True,
|
||||
message_type='logstash', lvl='INFO',
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
handler = HTTPSHandler(host='127.0.0.1', message_type='logstash')
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
handler.session.mount('http://', connection_error_adapter)
|
||||
|
||||
@ -289,9 +261,7 @@ def test_https_logging_handler_connection_error(connection_error_adapter,
|
||||
@pytest.mark.parametrize('message_type', ['logstash', 'splunk'])
|
||||
def test_https_logging_handler_emit_without_cred(http_adapter, dummy_log_record,
|
||||
message_type):
|
||||
handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True,
|
||||
message_type=message_type, lvl='INFO',
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
handler = HTTPSHandler(host='127.0.0.1', message_type=message_type)
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
handler.session.mount('http://', http_adapter)
|
||||
async_futures = handler.emit(dummy_log_record)
|
||||
@ -312,10 +282,9 @@ def test_https_logging_handler_emit_without_cred(http_adapter, dummy_log_record,
|
||||
|
||||
def test_https_logging_handler_emit_logstash_with_creds(http_adapter,
|
||||
dummy_log_record):
|
||||
handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True,
|
||||
handler = HTTPSHandler(host='127.0.0.1',
|
||||
username='user', password='pass',
|
||||
message_type='logstash', lvl='INFO',
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
message_type='logstash')
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
handler.session.mount('http://', http_adapter)
|
||||
async_futures = handler.emit(dummy_log_record)
|
||||
@ -328,9 +297,8 @@ def test_https_logging_handler_emit_logstash_with_creds(http_adapter,
|
||||
|
||||
def test_https_logging_handler_emit_splunk_with_creds(http_adapter,
|
||||
dummy_log_record):
|
||||
handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True,
|
||||
password='pass', message_type='splunk', lvl='INFO',
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
handler = HTTPSHandler(host='127.0.0.1',
|
||||
password='pass', message_type='splunk')
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
handler.session.mount('http://', http_adapter)
|
||||
async_futures = handler.emit(dummy_log_record)
|
||||
@ -351,9 +319,7 @@ def test_encode_payload_for_socket(payload, encoded_payload):
|
||||
|
||||
|
||||
def test_udp_handler_create_socket_at_init():
|
||||
handler = UDPHandler(host='127.0.0.1', port=4399,
|
||||
enabled_flag=True, message_type='splunk', lvl='INFO',
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
handler = UDPHandler(host='127.0.0.1', port=4399)
|
||||
assert hasattr(handler, 'socket')
|
||||
assert isinstance(handler.socket, socket.socket)
|
||||
assert handler.socket.family == socket.AF_INET
|
||||
@ -361,9 +327,7 @@ def test_udp_handler_create_socket_at_init():
|
||||
|
||||
|
||||
def test_udp_handler_send(dummy_log_record):
|
||||
handler = UDPHandler(host='127.0.0.1', port=4399,
|
||||
enabled_flag=True, message_type='splunk', lvl='INFO',
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
handler = UDPHandler(host='127.0.0.1', port=4399)
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
with mock.patch('awx.main.utils.handlers._encode_payload_for_socket', return_value="des") as encode_mock,\
|
||||
mock.patch.object(handler, 'socket') as socket_mock:
|
||||
@ -373,9 +337,7 @@ def test_udp_handler_send(dummy_log_record):
|
||||
|
||||
|
||||
def test_tcp_handler_send(fake_socket, dummy_log_record):
|
||||
handler = TCPHandler(host='127.0.0.1', port=4399, tcp_timeout=5,
|
||||
enabled_flag=True, message_type='splunk', lvl='INFO',
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
handler = TCPHandler(host='127.0.0.1', port=4399, tcp_timeout=5)
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
with mock.patch('socket.socket', return_value=fake_socket) as sok_init_mock,\
|
||||
mock.patch('select.select', return_value=([], [fake_socket], [])):
|
||||
@ -388,9 +350,7 @@ def test_tcp_handler_send(fake_socket, dummy_log_record):
|
||||
|
||||
|
||||
def test_tcp_handler_return_if_socket_unavailable(fake_socket, dummy_log_record):
|
||||
handler = TCPHandler(host='127.0.0.1', port=4399, tcp_timeout=5,
|
||||
enabled_flag=True, message_type='splunk', lvl='INFO',
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
handler = TCPHandler(host='127.0.0.1', port=4399, tcp_timeout=5)
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
with mock.patch('socket.socket', return_value=fake_socket) as sok_init_mock,\
|
||||
mock.patch('select.select', return_value=([], [], [])):
|
||||
@ -403,9 +363,7 @@ def test_tcp_handler_return_if_socket_unavailable(fake_socket, dummy_log_record)
|
||||
|
||||
|
||||
def test_tcp_handler_log_exception(fake_socket, dummy_log_record):
|
||||
handler = TCPHandler(host='127.0.0.1', port=4399, tcp_timeout=5,
|
||||
enabled_flag=True, message_type='splunk', lvl='INFO',
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
handler = TCPHandler(host='127.0.0.1', port=4399, tcp_timeout=5)
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
with mock.patch('socket.socket', return_value=fake_socket) as sok_init_mock,\
|
||||
mock.patch('select.select', return_value=([], [], [])),\
|
||||
|
||||
@ -13,31 +13,3 @@ def test_produce_supervisor_command(mocker):
|
||||
['supervisorctl', 'restart', 'tower-processes:receiver',],
|
||||
stderr=-1, stdin=-1, stdout=-1)
|
||||
|
||||
|
||||
def test_routing_of_service_restarts_works(mocker):
|
||||
'''
|
||||
This tests that the parent restart method will call the appropriate
|
||||
service restart methods, depending on which services are given in args
|
||||
'''
|
||||
with mocker.patch.object(reload, '_uwsgi_fifo_command'),\
|
||||
mocker.patch.object(reload, '_reset_celery_thread_pool'),\
|
||||
mocker.patch.object(reload, '_supervisor_service_command'):
|
||||
reload.restart_local_services(['uwsgi', 'celery', 'flower', 'daphne'])
|
||||
reload._uwsgi_fifo_command.assert_called_once_with(uwsgi_command="c")
|
||||
reload._reset_celery_thread_pool.assert_called_once_with()
|
||||
reload._supervisor_service_command.assert_called_once_with(['flower', 'daphne'], command="restart")
|
||||
|
||||
|
||||
|
||||
def test_routing_of_service_restarts_diables(mocker):
|
||||
'''
|
||||
Test that methods are not called if not in the args
|
||||
'''
|
||||
with mocker.patch.object(reload, '_uwsgi_fifo_command'),\
|
||||
mocker.patch.object(reload, '_reset_celery_thread_pool'),\
|
||||
mocker.patch.object(reload, '_supervisor_service_command'):
|
||||
reload.restart_local_services(['flower'])
|
||||
reload._uwsgi_fifo_command.assert_not_called()
|
||||
reload._reset_celery_thread_pool.assert_not_called()
|
||||
reload._supervisor_service_command.assert_called_once_with(['flower'], command="restart")
|
||||
|
||||
|
||||
@ -127,12 +127,16 @@ class IllegalArgumentError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def get_memoize_cache():
|
||||
from django.core.cache import cache
|
||||
return cache
|
||||
|
||||
|
||||
def memoize(ttl=60, cache_key=None, track_function=False):
|
||||
'''
|
||||
Decorator to wrap a function and cache its result.
|
||||
'''
|
||||
from django.core.cache import cache
|
||||
|
||||
cache = get_memoize_cache()
|
||||
|
||||
def _memoizer(f, *args, **kwargs):
|
||||
if cache_key and track_function:
|
||||
@ -160,8 +164,7 @@ def memoize(ttl=60, cache_key=None, track_function=False):
|
||||
|
||||
|
||||
def memoize_delete(function_name):
|
||||
from django.core.cache import cache
|
||||
|
||||
cache = get_memoize_cache()
|
||||
return cache.delete(function_name)
|
||||
|
||||
|
||||
|
||||
@ -8,14 +8,106 @@ from pyparsing import (
|
||||
CharsNotIn,
|
||||
ParseException,
|
||||
)
|
||||
from logging import Filter, _levelNames
|
||||
|
||||
import six
|
||||
|
||||
import django
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
from awx.main.utils.common import get_search_fields
|
||||
|
||||
__all__ = ['SmartFilter']
|
||||
__all__ = ['SmartFilter', 'ExternalLoggerEnabled']
|
||||
|
||||
|
||||
class FieldFromSettings(object):
|
||||
"""
|
||||
Field interface - defaults to getting value from setting
|
||||
if otherwise set, provided value will take precedence
|
||||
over value in settings
|
||||
"""
|
||||
|
||||
def __init__(self, setting_name):
|
||||
self.setting_name = setting_name
|
||||
|
||||
def __get__(self, instance, type=None):
|
||||
if self.setting_name in getattr(instance, 'settings_override', {}):
|
||||
return instance.settings_override[self.setting_name]
|
||||
return getattr(settings, self.setting_name, None)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
if value is None:
|
||||
if hasattr(instance, 'settings_override'):
|
||||
instance.settings_override.pop('instance', None)
|
||||
else:
|
||||
if not hasattr(instance, 'settings_override'):
|
||||
instance.settings_override = {}
|
||||
instance.settings_override[self.setting_name] = value
|
||||
|
||||
|
||||
class ExternalLoggerEnabled(Filter):
|
||||
|
||||
# Prevents recursive logging loops from swamping the server
|
||||
LOGGER_BLACKLIST = (
|
||||
# loggers that may be called in process of emitting a log
|
||||
'awx.main.utils.handlers',
|
||||
'awx.main.utils.formatters',
|
||||
'awx.main.utils.filters',
|
||||
'awx.main.utils.encryption',
|
||||
'awx.main.utils.log',
|
||||
# loggers that may be called getting logging settings
|
||||
'awx.conf'
|
||||
)
|
||||
|
||||
lvl = FieldFromSettings('LOG_AGGREGATOR_LEVEL')
|
||||
enabled_loggers = FieldFromSettings('LOG_AGGREGATOR_LOGGERS')
|
||||
enabled_flag = FieldFromSettings('LOG_AGGREGATOR_ENABLED')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(ExternalLoggerEnabled, self).__init__()
|
||||
for field_name, field_value in kwargs.items():
|
||||
if not isinstance(ExternalLoggerEnabled.__dict__.get(field_name, None), FieldFromSettings):
|
||||
raise Exception('%s is not a valid kwarg' % field_name)
|
||||
if field_value is None:
|
||||
continue
|
||||
setattr(self, field_name, field_value)
|
||||
|
||||
def filter(self, record):
|
||||
"""
|
||||
Uses the database settings to determine if the current
|
||||
external log configuration says that this particular record
|
||||
should be sent to the external log aggregator
|
||||
|
||||
False - should not be logged
|
||||
True - should be logged
|
||||
"""
|
||||
# Logger exceptions
|
||||
for logger_name in self.LOGGER_BLACKLIST:
|
||||
if record.name.startswith(logger_name):
|
||||
return False
|
||||
# General enablement
|
||||
if not self.enabled_flag:
|
||||
return False
|
||||
|
||||
# Level enablement
|
||||
if record.levelno < _levelNames[self.lvl]:
|
||||
# logging._levelNames -> logging._nameToLevel in python 3
|
||||
return False
|
||||
|
||||
# Logger type enablement
|
||||
loggers = self.enabled_loggers
|
||||
if not loggers:
|
||||
return False
|
||||
if record.name.startswith('awx.analytics'):
|
||||
base_path, headline_name = record.name.rsplit('.', 1)
|
||||
return bool(headline_name in loggers)
|
||||
else:
|
||||
if '.' in record.name:
|
||||
base_name, trailing_path = record.name.split('.', 1)
|
||||
else:
|
||||
base_name = record.name
|
||||
return bool(base_name in loggers)
|
||||
|
||||
|
||||
def string_to_type(t):
|
||||
@ -36,7 +128,7 @@ def string_to_type(t):
|
||||
|
||||
|
||||
def get_model(name):
|
||||
return django.apps.apps.get_model('main', name)
|
||||
return apps.get_model('main', name)
|
||||
|
||||
|
||||
class SmartFilter(object):
|
||||
@ -52,7 +144,7 @@ class SmartFilter(object):
|
||||
search_kwargs = self._expand_search(k, v)
|
||||
if search_kwargs:
|
||||
kwargs.update(search_kwargs)
|
||||
q = reduce(lambda x, y: x | y, [django.db.models.Q(**{u'%s__contains' % _k:_v}) for _k, _v in kwargs.items()])
|
||||
q = reduce(lambda x, y: x | y, [models.Q(**{u'%s__contains' % _k:_v}) for _k, _v in kwargs.items()])
|
||||
self.result = Host.objects.filter(q)
|
||||
else:
|
||||
kwargs[k] = v
|
||||
|
||||
@ -9,6 +9,8 @@ import logging
|
||||
|
||||
import six
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class TimeFormatter(logging.Formatter):
|
||||
'''
|
||||
@ -20,15 +22,6 @@ class TimeFormatter(logging.Formatter):
|
||||
|
||||
|
||||
class LogstashFormatter(LogstashFormatterVersion1):
|
||||
def __init__(self, **kwargs):
|
||||
settings_module = kwargs.pop('settings_module', None)
|
||||
ret = super(LogstashFormatter, self).__init__(**kwargs)
|
||||
if settings_module:
|
||||
self.host_id = getattr(settings_module, 'CLUSTER_HOST_ID', None)
|
||||
if hasattr(settings_module, 'LOG_AGGREGATOR_TOWER_UUID'):
|
||||
self.tower_uuid = settings_module.LOG_AGGREGATOR_TOWER_UUID
|
||||
self.message_type = getattr(settings_module, 'LOG_AGGREGATOR_TYPE', 'other')
|
||||
return ret
|
||||
|
||||
def reformat_data_for_log(self, raw_data, kind=None):
|
||||
'''
|
||||
@ -147,6 +140,15 @@ class LogstashFormatter(LogstashFormatterVersion1):
|
||||
if record.name.startswith('awx.analytics'):
|
||||
log_kind = record.name[len('awx.analytics.'):]
|
||||
fields = self.reformat_data_for_log(fields, kind=log_kind)
|
||||
# General AWX metadata
|
||||
for log_name, setting_name in [
|
||||
('type', 'LOG_AGGREGATOR_TYPE'),
|
||||
('cluster_host_id', 'CLUSTER_HOST_ID'),
|
||||
('tower_uuid', 'LOG_AGGREGATOR_TOWER_UUID')]:
|
||||
if hasattr(settings, setting_name):
|
||||
fields[log_name] = getattr(settings, setting_name, None)
|
||||
elif log_name == 'type':
|
||||
fields[log_name] = 'other'
|
||||
return fields
|
||||
|
||||
def format(self, record):
|
||||
@ -158,18 +160,12 @@ class LogstashFormatter(LogstashFormatterVersion1):
|
||||
'@timestamp': self.format_timestamp(record.created),
|
||||
'message': record.getMessage(),
|
||||
'host': self.host,
|
||||
'type': self.message_type,
|
||||
|
||||
# Extra Fields
|
||||
'level': record.levelname,
|
||||
'logger_name': record.name,
|
||||
}
|
||||
|
||||
if getattr(self, 'tower_uuid', None):
|
||||
message['tower_uuid'] = self.tower_uuid
|
||||
if getattr(self, 'host_id', None):
|
||||
message['cluster_host_id'] = self.host_id
|
||||
|
||||
# Add extra fields
|
||||
message.update(self.get_extra_fields(record))
|
||||
|
||||
|
||||
@ -10,6 +10,10 @@ from django.conf import settings
|
||||
from awx.main.models import Instance
|
||||
|
||||
|
||||
def construct_bcast_queue_name(common_name):
|
||||
return common_name.encode('utf8') + '_' + settings.CLUSTER_HOST_ID
|
||||
|
||||
|
||||
def _add_remove_celery_worker_queues(app, controlled_instances, worker_queues, worker_name):
|
||||
removed_queues = []
|
||||
added_queues = []
|
||||
@ -19,17 +23,15 @@ def _add_remove_celery_worker_queues(app, controlled_instances, worker_queues, w
|
||||
ig_names.update(instance.rampart_groups.values_list('name', flat=True))
|
||||
worker_queue_names = set([q['name'] for q in worker_queues])
|
||||
|
||||
bcast_queue_names = set([construct_bcast_queue_name(n) for n in settings.AWX_CELERY_BCAST_QUEUES_STATIC])
|
||||
all_queue_names = ig_names | hostnames | set(settings.AWX_CELERY_QUEUES_STATIC)
|
||||
desired_queues = bcast_queue_names | (all_queue_names if instance.enabled else set())
|
||||
|
||||
# Remove queues that aren't in the instance group
|
||||
for queue in worker_queues:
|
||||
if queue['name'] in settings.AWX_CELERY_QUEUES_STATIC or \
|
||||
queue['alias'] in settings.AWX_CELERY_BCAST_QUEUES_STATIC:
|
||||
continue
|
||||
|
||||
if queue['name'] not in all_queue_names or not instance.enabled:
|
||||
app.control.cancel_consumer(queue['name'].encode("utf8"), reply=True, destination=[worker_name])
|
||||
removed_queues.append(queue['name'].encode("utf8"))
|
||||
# Remove queues
|
||||
for queue_name in worker_queue_names:
|
||||
if queue_name not in desired_queues:
|
||||
app.control.cancel_consumer(queue_name.encode("utf8"), reply=True, destination=[worker_name])
|
||||
removed_queues.append(queue_name.encode("utf8"))
|
||||
|
||||
# Add queues for instance and instance groups
|
||||
for queue_name in all_queue_names:
|
||||
@ -37,27 +39,35 @@ def _add_remove_celery_worker_queues(app, controlled_instances, worker_queues, w
|
||||
app.control.add_consumer(queue_name.encode("utf8"), reply=True, destination=[worker_name])
|
||||
added_queues.append(queue_name.encode("utf8"))
|
||||
|
||||
# Add stable-named broadcast queues
|
||||
for queue_name in settings.AWX_CELERY_BCAST_QUEUES_STATIC:
|
||||
bcast_queue_name = construct_bcast_queue_name(queue_name)
|
||||
if bcast_queue_name not in worker_queue_names:
|
||||
app.control.add_consumer(bcast_queue_name,
|
||||
exchange=queue_name.encode("utf8"),
|
||||
exchange_type='fanout',
|
||||
routing_key=queue_name.encode("utf8"),
|
||||
reply=True)
|
||||
added_queues.append(bcast_queue_name)
|
||||
|
||||
return (added_queues, removed_queues)
|
||||
|
||||
|
||||
def update_celery_worker_routes(instance, conf):
|
||||
tasks = [
|
||||
'awx.main.tasks.cluster_node_heartbeat',
|
||||
'awx.main.tasks.purge_old_stdout_files',
|
||||
]
|
||||
routes_updated = {}
|
||||
# Instance is, effectively, a controller node
|
||||
if instance.is_controller():
|
||||
tasks.append('awx.main.tasks.awx_isolated_heartbeat')
|
||||
else:
|
||||
if 'awx.main.tasks.awx_isolated_heartbeat' in conf.CELERY_ROUTES:
|
||||
del conf.CELERY_ROUTES['awx.main.tasks.awx_isolated_heartbeat']
|
||||
class AWXCeleryRouter(object):
|
||||
def route_for_task(self, task, args=None, kwargs=None):
|
||||
(changed, instance) = Instance.objects.get_or_register()
|
||||
tasks = [
|
||||
'awx.main.tasks.cluster_node_heartbeat',
|
||||
'awx.main.tasks.purge_old_stdout_files',
|
||||
]
|
||||
isolated_tasks = [
|
||||
'awx.main.tasks.awx_isolated_heartbeat',
|
||||
]
|
||||
if task in tasks:
|
||||
return {'queue': instance.hostname.encode("utf8"), 'routing_key': instance.hostname.encode("utf8")}
|
||||
|
||||
for t in tasks:
|
||||
conf.CELERY_ROUTES[t] = {'queue': instance.hostname.encode("utf8"), 'routing_key': instance.hostname.encode("utf8")}
|
||||
routes_updated[t] = conf.CELERY_ROUTES[t]
|
||||
|
||||
return routes_updated
|
||||
if instance.is_controller() and task in isolated_tasks:
|
||||
return {'queue': instance.hostname.encode("utf8"), 'routing_key': instance.hostname.encode("utf8")}
|
||||
|
||||
|
||||
def register_celery_worker_queues(app, celery_worker_name):
|
||||
|
||||
@ -13,40 +13,35 @@ import six
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# loggly
|
||||
import traceback
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
|
||||
# requests futures, a dependency used by these handlers
|
||||
from requests_futures.sessions import FuturesSession
|
||||
|
||||
# AWX
|
||||
from awx.main.utils.formatters import LogstashFormatter
|
||||
|
||||
|
||||
__all__ = ['HTTPSNullHandler', 'BaseHTTPSHandler', 'TCPHandler', 'UDPHandler',
|
||||
'configure_external_logger']
|
||||
__all__ = ['BaseHTTPSHandler', 'TCPHandler', 'UDPHandler',
|
||||
'AWXProxyHandler']
|
||||
|
||||
|
||||
logger = logging.getLogger('awx.main.utils.handlers')
|
||||
|
||||
# AWX external logging handler, generally designed to be used
|
||||
# with the accompanying LogstashHandler, derives from python-logstash library
|
||||
# Non-blocking request accomplished by FuturesSession, similar
|
||||
# to the loggly-python-handler library (not used)
|
||||
|
||||
# Translation of parameter names to names in Django settings
|
||||
# logging settings category, only those related to handler / log emission
|
||||
PARAM_NAMES = {
|
||||
'host': 'LOG_AGGREGATOR_HOST',
|
||||
'port': 'LOG_AGGREGATOR_PORT',
|
||||
'message_type': 'LOG_AGGREGATOR_TYPE',
|
||||
'username': 'LOG_AGGREGATOR_USERNAME',
|
||||
'password': 'LOG_AGGREGATOR_PASSWORD',
|
||||
'enabled_loggers': 'LOG_AGGREGATOR_LOGGERS',
|
||||
'indv_facts': 'LOG_AGGREGATOR_INDIVIDUAL_FACTS',
|
||||
'enabled_flag': 'LOG_AGGREGATOR_ENABLED',
|
||||
'tcp_timeout': 'LOG_AGGREGATOR_TCP_TIMEOUT',
|
||||
'verify_cert': 'LOG_AGGREGATOR_VERIFY_CERT',
|
||||
'lvl': 'LOG_AGGREGATOR_LEVEL',
|
||||
'protocol': 'LOG_AGGREGATOR_PROTOCOL'
|
||||
}
|
||||
|
||||
|
||||
@ -58,13 +53,6 @@ class LoggingConnectivityException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class HTTPSNullHandler(logging.NullHandler):
|
||||
"Placeholder null handler to allow loading without database access"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
return super(HTTPSNullHandler, self).__init__()
|
||||
|
||||
|
||||
class VerboseThreadPoolExecutor(ThreadPoolExecutor):
|
||||
|
||||
last_log_emit = 0
|
||||
@ -91,32 +79,25 @@ class VerboseThreadPoolExecutor(ThreadPoolExecutor):
|
||||
**kwargs)
|
||||
|
||||
|
||||
LEVEL_MAPPING = {
|
||||
'DEBUG': logging.DEBUG,
|
||||
'INFO': logging.INFO,
|
||||
'WARNING': logging.WARNING,
|
||||
'ERROR': logging.ERROR,
|
||||
'CRITICAL': logging.CRITICAL,
|
||||
}
|
||||
class SocketResult:
|
||||
'''
|
||||
A class to be the return type of methods that send data over a socket
|
||||
allows object to be used in the same way as a request futures object
|
||||
'''
|
||||
def __init__(self, ok, reason=None):
|
||||
self.ok = ok
|
||||
self.reason = reason
|
||||
|
||||
def result(self):
|
||||
return self
|
||||
|
||||
|
||||
class BaseHandler(logging.Handler):
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, host=None, port=None, indv_facts=None, **kwargs):
|
||||
super(BaseHandler, self).__init__()
|
||||
for fd in PARAM_NAMES:
|
||||
setattr(self, fd, kwargs.get(fd, None))
|
||||
|
||||
@classmethod
|
||||
def from_django_settings(cls, settings, *args, **kwargs):
|
||||
for param, django_setting_name in PARAM_NAMES.items():
|
||||
kwargs[param] = getattr(settings, django_setting_name, None)
|
||||
return cls(*args, **kwargs)
|
||||
|
||||
def get_full_message(self, record):
|
||||
if record.exc_info:
|
||||
return '\n'.join(traceback.format_exception(*record.exc_info))
|
||||
else:
|
||||
return record.getMessage()
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.indv_facts = indv_facts
|
||||
|
||||
def _send(self, payload):
|
||||
"""Actually send message to log aggregator.
|
||||
@ -128,26 +109,11 @@ class BaseHandler(logging.Handler):
|
||||
return [self._send(json.loads(self.format(record)))]
|
||||
return [self._send(self.format(record))]
|
||||
|
||||
def _skip_log(self, logger_name):
|
||||
if self.host == '' or (not self.enabled_flag):
|
||||
return True
|
||||
# Don't send handler-related records.
|
||||
if logger_name == logger.name:
|
||||
return True
|
||||
# AWX log emission is only turned off by enablement setting
|
||||
if not logger_name.startswith('awx.analytics'):
|
||||
return False
|
||||
return self.enabled_loggers is None or logger_name[len('awx.analytics.'):] not in self.enabled_loggers
|
||||
|
||||
def emit(self, record):
|
||||
"""
|
||||
Emit a log record. Returns a list of zero or more
|
||||
implementation-specific objects for tests.
|
||||
"""
|
||||
if not record.name.startswith('awx.analytics') and record.levelno < LEVEL_MAPPING[self.lvl]:
|
||||
return []
|
||||
if self._skip_log(record.name):
|
||||
return []
|
||||
try:
|
||||
return self._format_and_send_record(record)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
@ -181,6 +147,11 @@ class BaseHandler(logging.Handler):
|
||||
|
||||
|
||||
class BaseHTTPSHandler(BaseHandler):
|
||||
'''
|
||||
Originally derived from python-logstash library
|
||||
Non-blocking request accomplished by FuturesSession, similar
|
||||
to the loggly-python-handler library
|
||||
'''
|
||||
def _add_auth_information(self):
|
||||
if self.message_type == 'logstash':
|
||||
if not self.username:
|
||||
@ -196,39 +167,20 @@ class BaseHTTPSHandler(BaseHandler):
|
||||
}
|
||||
self.session.headers.update(headers)
|
||||
|
||||
def __init__(self, fqdn=False, **kwargs):
|
||||
def __init__(self, fqdn=False, message_type=None, username=None, password=None,
|
||||
tcp_timeout=5, verify_cert=True, **kwargs):
|
||||
self.fqdn = fqdn
|
||||
self.message_type = message_type
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.tcp_timeout = tcp_timeout
|
||||
self.verify_cert = verify_cert
|
||||
super(BaseHTTPSHandler, self).__init__(**kwargs)
|
||||
self.session = FuturesSession(executor=VerboseThreadPoolExecutor(
|
||||
max_workers=2 # this is the default used by requests_futures
|
||||
))
|
||||
self._add_auth_information()
|
||||
|
||||
@classmethod
|
||||
def perform_test(cls, settings):
|
||||
"""
|
||||
Tests logging connectivity for the current logging settings.
|
||||
@raises LoggingConnectivityException
|
||||
"""
|
||||
handler = cls.from_django_settings(settings)
|
||||
handler.enabled_flag = True
|
||||
handler.setFormatter(LogstashFormatter(settings_module=settings))
|
||||
logger = logging.getLogger(__file__)
|
||||
fn, lno, func = logger.findCaller()
|
||||
record = logger.makeRecord('awx', 10, fn, lno,
|
||||
'AWX Connection Test', tuple(),
|
||||
None, func)
|
||||
futures = handler.emit(record)
|
||||
for future in futures:
|
||||
try:
|
||||
resp = future.result()
|
||||
if not resp.ok:
|
||||
raise LoggingConnectivityException(
|
||||
': '.join([str(resp.status_code), resp.reason or ''])
|
||||
)
|
||||
except RequestException as e:
|
||||
raise LoggingConnectivityException(str(e))
|
||||
|
||||
def _get_post_kwargs(self, payload_input):
|
||||
if self.message_type == 'splunk':
|
||||
# Splunk needs data nested under key "event"
|
||||
@ -265,6 +217,10 @@ def _encode_payload_for_socket(payload):
|
||||
|
||||
|
||||
class TCPHandler(BaseHandler):
|
||||
def __init__(self, tcp_timeout=5, **kwargs):
|
||||
self.tcp_timeout = tcp_timeout
|
||||
super(TCPHandler, self).__init__(**kwargs)
|
||||
|
||||
def _send(self, payload):
|
||||
payload = _encode_payload_for_socket(payload)
|
||||
sok = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
@ -273,39 +229,32 @@ class TCPHandler(BaseHandler):
|
||||
sok.setblocking(0)
|
||||
_, ready_to_send, _ = select.select([], [sok], [], float(self.tcp_timeout))
|
||||
if len(ready_to_send) == 0:
|
||||
logger.warning("Socket currently busy, failed to send message")
|
||||
sok.close()
|
||||
return
|
||||
sok.send(payload)
|
||||
ret = SocketResult(False, "Socket currently busy, failed to send message")
|
||||
logger.warning(ret.reason)
|
||||
else:
|
||||
sok.send(payload)
|
||||
ret = SocketResult(True) # success!
|
||||
except Exception as e:
|
||||
logger.exception("Error sending message from %s: %s" %
|
||||
(TCPHandler.__name__, e.message))
|
||||
sok.close()
|
||||
ret = SocketResult(False, "Error sending message from %s: %s" %
|
||||
(TCPHandler.__name__,
|
||||
' '.join(six.text_type(arg) for arg in e.args)))
|
||||
logger.exception(ret.reason)
|
||||
finally:
|
||||
sok.close()
|
||||
return ret
|
||||
|
||||
|
||||
class UDPHandler(BaseHandler):
|
||||
message = "Cannot determine if UDP messages are received."
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(UDPHandler, self).__init__(**kwargs)
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
def _send(self, payload):
|
||||
payload = _encode_payload_for_socket(payload)
|
||||
return self.socket.sendto(payload, (self._get_host(hostname_only=True), self.port or 0))
|
||||
|
||||
@classmethod
|
||||
def perform_test(cls, settings):
|
||||
"""
|
||||
Tests logging connectivity for the current logging settings.
|
||||
"""
|
||||
handler = cls.from_django_settings(settings)
|
||||
handler.enabled_flag = True
|
||||
handler.setFormatter(LogstashFormatter(settings_module=settings))
|
||||
logger = logging.getLogger(__file__)
|
||||
fn, lno, func = logger.findCaller()
|
||||
record = logger.makeRecord('awx', 10, fn, lno,
|
||||
'AWX Connection Test', tuple(),
|
||||
None, func)
|
||||
handler.emit(_encode_payload_for_socket(record))
|
||||
self.socket.sendto(payload, (self._get_host(hostname_only=True), self.port or 0))
|
||||
return SocketResult(True, reason=self.message)
|
||||
|
||||
|
||||
HANDLER_MAPPING = {
|
||||
@ -315,6 +264,88 @@ HANDLER_MAPPING = {
|
||||
}
|
||||
|
||||
|
||||
class AWXProxyHandler(logging.Handler):
|
||||
'''
|
||||
Handler specific to the AWX external logging feature
|
||||
|
||||
Will dynamically create a handler specific to the configured
|
||||
protocol, and will create a new one automatically on setting change
|
||||
|
||||
Managing parameters:
|
||||
All parameters will get their value from settings as a default
|
||||
if the parameter was either provided on init, or set manually,
|
||||
this value will take precedence.
|
||||
Parameters match same parameters in the actualized handler classes.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# TODO: process 'level' kwarg
|
||||
super(AWXProxyHandler, self).__init__(**kwargs)
|
||||
self._handler = None
|
||||
self._old_kwargs = {}
|
||||
|
||||
def get_handler_class(self, protocol):
|
||||
return HANDLER_MAPPING[protocol]
|
||||
|
||||
def get_handler(self, custom_settings=None, force_create=False):
|
||||
new_kwargs = {}
|
||||
use_settings = custom_settings or settings
|
||||
for field_name, setting_name in PARAM_NAMES.items():
|
||||
val = getattr(use_settings, setting_name, None)
|
||||
if val is None:
|
||||
continue
|
||||
new_kwargs[field_name] = val
|
||||
if new_kwargs == self._old_kwargs and self._handler and (not force_create):
|
||||
# avoids re-creating session objects, and other such things
|
||||
return self._handler
|
||||
self._old_kwargs = new_kwargs.copy()
|
||||
# TODO: remove any kwargs no applicable to that particular handler
|
||||
protocol = new_kwargs.pop('protocol', None)
|
||||
HandlerClass = self.get_handler_class(protocol)
|
||||
# cleanup old handler and make new one
|
||||
if self._handler:
|
||||
self._handler.close()
|
||||
logger.debug('Creating external log handler due to startup or settings change.')
|
||||
self._handler = HandlerClass(**new_kwargs)
|
||||
if self.formatter:
|
||||
# self.format(record) is called inside of emit method
|
||||
# so not safe to assume this can be handled within self
|
||||
self._handler.setFormatter(self.formatter)
|
||||
return self._handler
|
||||
|
||||
def emit(self, record):
|
||||
actual_handler = self.get_handler()
|
||||
return actual_handler.emit(record)
|
||||
|
||||
def perform_test(self, custom_settings):
|
||||
"""
|
||||
Tests logging connectivity for given settings module.
|
||||
@raises LoggingConnectivityException
|
||||
"""
|
||||
handler = self.get_handler(custom_settings=custom_settings, force_create=True)
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
logger = logging.getLogger(__file__)
|
||||
fn, lno, func = logger.findCaller()
|
||||
record = logger.makeRecord('awx', 10, fn, lno,
|
||||
'AWX Connection Test', tuple(),
|
||||
None, func)
|
||||
futures = handler.emit(record)
|
||||
for future in futures:
|
||||
try:
|
||||
resp = future.result()
|
||||
if not resp.ok:
|
||||
if isinstance(resp, SocketResult):
|
||||
raise LoggingConnectivityException(
|
||||
'Socket error: {}'.format(resp.reason or '')
|
||||
)
|
||||
else:
|
||||
raise LoggingConnectivityException(
|
||||
': '.join([str(resp.status_code), resp.reason or ''])
|
||||
)
|
||||
except RequestException as e:
|
||||
raise LoggingConnectivityException(str(e))
|
||||
|
||||
|
||||
ColorHandler = logging.StreamHandler
|
||||
|
||||
if settings.COLOR_LOGS is True:
|
||||
@ -340,41 +371,3 @@ if settings.COLOR_LOGS is True:
|
||||
except ImportError:
|
||||
# logutils is only used for colored logs in the dev environment
|
||||
pass
|
||||
|
||||
|
||||
def _add_or_remove_logger(address, instance):
|
||||
specific_logger = logging.getLogger(address)
|
||||
for i, handler in enumerate(specific_logger.handlers):
|
||||
if isinstance(handler, (HTTPSNullHandler, BaseHTTPSHandler)):
|
||||
specific_logger.handlers[i] = instance or HTTPSNullHandler()
|
||||
break
|
||||
else:
|
||||
if instance is not None:
|
||||
specific_logger.handlers.append(instance)
|
||||
|
||||
|
||||
def configure_external_logger(settings_module, is_startup=True):
|
||||
is_enabled = settings_module.LOG_AGGREGATOR_ENABLED
|
||||
if is_startup and (not is_enabled):
|
||||
# Pass-through if external logging not being used
|
||||
return
|
||||
|
||||
instance = None
|
||||
if is_enabled:
|
||||
handler_class = HANDLER_MAPPING[settings_module.LOG_AGGREGATOR_PROTOCOL]
|
||||
instance = handler_class.from_django_settings(settings_module)
|
||||
|
||||
# Obtain the Formatter class from settings to maintain customizations
|
||||
configurator = logging.config.DictConfigurator(settings_module.LOGGING)
|
||||
formatter_config = settings_module.LOGGING['formatters']['json'].copy()
|
||||
formatter_config['settings_module'] = settings_module
|
||||
formatter = configurator.configure_custom(formatter_config)
|
||||
|
||||
instance.setFormatter(formatter)
|
||||
|
||||
awx_logger_instance = instance
|
||||
if is_enabled and 'awx' not in settings_module.LOG_AGGREGATOR_LOGGERS:
|
||||
awx_logger_instance = None
|
||||
|
||||
_add_or_remove_logger('awx.analytics', instance)
|
||||
_add_or_remove_logger('awx', awx_logger_instance)
|
||||
|
||||
@ -8,29 +8,9 @@ import logging
|
||||
# Django
|
||||
from django.conf import settings
|
||||
|
||||
# Celery
|
||||
from celery import Celery
|
||||
|
||||
logger = logging.getLogger('awx.main.utils.reload')
|
||||
|
||||
|
||||
def _uwsgi_fifo_command(uwsgi_command):
|
||||
# http://uwsgi-docs.readthedocs.io/en/latest/MasterFIFO.html#available-commands
|
||||
logger.warn('Initiating uWSGI chain reload of server')
|
||||
TRIGGER_COMMAND = uwsgi_command
|
||||
with open(settings.UWSGI_FIFO_LOCATION, 'w') as awxfifo:
|
||||
awxfifo.write(TRIGGER_COMMAND)
|
||||
|
||||
|
||||
def _reset_celery_thread_pool():
|
||||
# Do not use current_app because of this outstanding issue:
|
||||
# https://github.com/celery/celery/issues/4410
|
||||
app = Celery('awx')
|
||||
app.config_from_object('django.conf:settings')
|
||||
app.control.broadcast('pool_restart', arguments={'reload': True},
|
||||
destination=['celery@{}'.format(settings.CLUSTER_HOST_ID)], reply=False)
|
||||
|
||||
|
||||
def _supervisor_service_command(service_internal_names, command, communicate=True):
|
||||
'''
|
||||
Service internal name options:
|
||||
@ -68,21 +48,6 @@ def _supervisor_service_command(service_internal_names, command, communicate=Tru
|
||||
logger.info('Submitted supervisorctl {} command, not waiting for result'.format(command))
|
||||
|
||||
|
||||
def restart_local_services(service_internal_names):
|
||||
logger.warn('Restarting services {} on this node in response to user action'.format(service_internal_names))
|
||||
if 'uwsgi' in service_internal_names:
|
||||
_uwsgi_fifo_command(uwsgi_command='c')
|
||||
service_internal_names.remove('uwsgi')
|
||||
restart_celery = False
|
||||
if 'celery' in service_internal_names:
|
||||
restart_celery = True
|
||||
service_internal_names.remove('celery')
|
||||
_supervisor_service_command(service_internal_names, command='restart')
|
||||
if restart_celery:
|
||||
# Celery restarted last because this probably includes current process
|
||||
_reset_celery_thread_pool()
|
||||
|
||||
|
||||
def stop_local_services(service_internal_names, communicate=True):
|
||||
logger.warn('Stopping services {} on this node in response to user action'.format(service_internal_names))
|
||||
_supervisor_service_command(service_internal_names, command='stop', communicate=communicate)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Copyright (c) 2017 Red Hat, Inc
|
||||
from channels import Group
|
||||
from channels.sessions import channel_session
|
||||
import channels
|
||||
from channels.auth import channel_session_user, channel_session_user_from_http
|
||||
from awx.network_ui.models import Topology, Device, Link, Client, Interface
|
||||
from awx.network_ui.models import TopologyInventory
|
||||
import urlparse
|
||||
@ -22,6 +22,10 @@ def parse_inventory_id(data):
|
||||
inventory_id = int(inventory_id[0])
|
||||
except ValueError:
|
||||
inventory_id = None
|
||||
except IndexError:
|
||||
inventory_id = None
|
||||
except TypeError:
|
||||
inventory_id = None
|
||||
if not inventory_id:
|
||||
inventory_id = None
|
||||
return inventory_id
|
||||
@ -42,10 +46,10 @@ class NetworkingEvents(object):
|
||||
message_type = data.pop(0)
|
||||
message_value = data.pop(0)
|
||||
if isinstance(message_value, list):
|
||||
logger.error("Message has no sender")
|
||||
logger.warning("Message has no sender")
|
||||
return None, None
|
||||
if isinstance(message_value, dict) and client_id != message_value.get('sender'):
|
||||
logger.error("client_id mismatch expected: %s actual %s", client_id, message_value.get('sender'))
|
||||
logger.warning("client_id mismatch expected: %s actual %s", client_id, message_value.get('sender'))
|
||||
return None, None
|
||||
return message_type, message_value
|
||||
else:
|
||||
@ -58,11 +62,19 @@ class NetworkingEvents(object):
|
||||
of name onX where X is the message type.
|
||||
'''
|
||||
topology_id = message.get('topology')
|
||||
assert topology_id is not None, "No topology_id"
|
||||
if topology_id is None:
|
||||
logger.warning("Unsupported message %s: no topology", message)
|
||||
return
|
||||
client_id = message.get('client')
|
||||
assert client_id is not None, "No client_id"
|
||||
if client_id is None:
|
||||
logger.warning("Unsupported message %s: no client", message)
|
||||
return
|
||||
if 'text' not in message:
|
||||
logger.warning("Unsupported message %s: no data", message)
|
||||
return
|
||||
message_type, message_value = self.parse_message_text(message['text'], client_id)
|
||||
if message_type is None:
|
||||
logger.warning("Unsupported message %s: no message type", message)
|
||||
return
|
||||
handler = self.get_handler(message_type)
|
||||
if handler is not None:
|
||||
@ -98,9 +110,6 @@ class NetworkingEvents(object):
|
||||
def onDeviceMove(self, device, topology_id, client_id):
|
||||
Device.objects.filter(topology_id=topology_id, cid=device['id']).update(x=device['x'], y=device['y'])
|
||||
|
||||
def onDeviceInventoryUpdate(self, device, topology_id, client_id):
|
||||
Device.objects.filter(topology_id=topology_id, cid=device['id']).update(host_id=device['host_id'])
|
||||
|
||||
def onDeviceLabelEdit(self, device, topology_id, client_id):
|
||||
logger.debug("Device label edited %s", device)
|
||||
Device.objects.filter(topology_id=topology_id, cid=device['id']).update(name=device['name'])
|
||||
@ -132,6 +141,12 @@ class NetworkingEvents(object):
|
||||
device_map = dict(Device.objects
|
||||
.filter(topology_id=topology_id, cid__in=[link['from_device_id'], link['to_device_id']])
|
||||
.values_list('cid', 'pk'))
|
||||
if link['from_device_id'] not in device_map:
|
||||
logger.warning('Device not found')
|
||||
return
|
||||
if link['to_device_id'] not in device_map:
|
||||
logger.warning('Device not found')
|
||||
return
|
||||
Link.objects.get_or_create(cid=link['id'],
|
||||
name=link['name'],
|
||||
from_device_id=device_map[link['from_device_id']],
|
||||
@ -150,8 +165,10 @@ class NetworkingEvents(object):
|
||||
.filter(topology_id=topology_id, cid__in=[link['from_device_id'], link['to_device_id']])
|
||||
.values_list('cid', 'pk'))
|
||||
if link['from_device_id'] not in device_map:
|
||||
logger.warning('Device not found')
|
||||
return
|
||||
if link['to_device_id'] not in device_map:
|
||||
logger.warning('Device not found')
|
||||
return
|
||||
Link.objects.filter(cid=link['id'],
|
||||
from_device_id=device_map[link['from_device_id']],
|
||||
@ -189,8 +206,15 @@ class NetworkingEvents(object):
|
||||
networking_events_dispatcher = NetworkingEvents()
|
||||
|
||||
|
||||
@channel_session
|
||||
@channel_session_user_from_http
|
||||
def ws_connect(message):
|
||||
if not message.user.is_authenticated():
|
||||
logger.error("Request user is not authenticated to use websocket.")
|
||||
message.reply_channel.send({"close": True})
|
||||
return
|
||||
else:
|
||||
message.reply_channel.send({"accept": True})
|
||||
|
||||
data = urlparse.parse_qs(message.content['query_string'])
|
||||
inventory_id = parse_inventory_id(data)
|
||||
topology_ids = list(TopologyInventory.objects.filter(inventory_id=inventory_id).values_list('pk', flat=True))
|
||||
@ -205,11 +229,11 @@ def ws_connect(message):
|
||||
TopologyInventory(inventory_id=inventory_id, topology_id=topology.pk).save()
|
||||
topology_id = topology.pk
|
||||
message.channel_session['topology_id'] = topology_id
|
||||
Group("topology-%s" % topology_id).add(message.reply_channel)
|
||||
channels.Group("topology-%s" % topology_id).add(message.reply_channel)
|
||||
client = Client()
|
||||
client.save()
|
||||
message.channel_session['client_id'] = client.pk
|
||||
Group("client-%s" % client.pk).add(message.reply_channel)
|
||||
channels.Group("client-%s" % client.pk).add(message.reply_channel)
|
||||
message.reply_channel.send({"text": json.dumps(["id", client.pk])})
|
||||
message.reply_channel.send({"text": json.dumps(["topology_id", topology_id])})
|
||||
topology_data = transform_dict(dict(id='topology_id',
|
||||
@ -268,18 +292,18 @@ def send_snapshot(channel, topology_id):
|
||||
channel.send({"text": json.dumps(["Snapshot", snapshot])})
|
||||
|
||||
|
||||
@channel_session
|
||||
@channel_session_user
|
||||
def ws_message(message):
|
||||
# Send to all clients editing the topology
|
||||
Group("topology-%s" % message.channel_session['topology_id']).send({"text": message['text']})
|
||||
channels.Group("topology-%s" % message.channel_session['topology_id']).send({"text": message['text']})
|
||||
# Send to networking_events handler
|
||||
networking_events_dispatcher.handle({"text": message['text'],
|
||||
"topology": message.channel_session['topology_id'],
|
||||
"client": message.channel_session['client_id']})
|
||||
|
||||
|
||||
@channel_session
|
||||
@channel_session_user
|
||||
def ws_disconnect(message):
|
||||
if 'topology_id' in message.channel_session:
|
||||
Group("topology-%s" % message.channel_session['topology_id']).discard(message.reply_channel)
|
||||
channels.Group("topology-%s" % message.channel_session['topology_id']).discard(message.reply_channel)
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ from channels.routing import route
|
||||
from awx.network_ui.consumers import ws_connect, ws_message, ws_disconnect
|
||||
|
||||
channel_routing = [
|
||||
route("websocket.connect", ws_connect, path=r"^/network_ui/topology"),
|
||||
route("websocket.receive", ws_message, path=r"^/network_ui/topology"),
|
||||
route("websocket.disconnect", ws_disconnect, path=r"^/network_ui/topology"),
|
||||
route("websocket.connect", ws_connect, path=r"^/network_ui/topology/"),
|
||||
route("websocket.receive", ws_message, path=r"^/network_ui/topology/"),
|
||||
route("websocket.disconnect", ws_disconnect, path=r"^/network_ui/topology/"),
|
||||
]
|
||||
|
||||
0
awx/network_ui/tests/__init__.py
Normal file
0
awx/network_ui/tests/__init__.py
Normal file
9
awx/network_ui/tests/conftest.py
Normal file
9
awx/network_ui/tests/conftest.py
Normal file
@ -0,0 +1,9 @@
|
||||
import pytest
|
||||
from mock import PropertyMock
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _disable_database_settings(mocker):
|
||||
m = mocker.patch('awx.conf.settings.SettingsWrapper.all_supported_settings', new_callable=PropertyMock)
|
||||
m.return_value = []
|
||||
|
||||
0
awx/network_ui/tests/unit/__init__.py
Normal file
0
awx/network_ui/tests/unit/__init__.py
Normal file
240
awx/network_ui/tests/unit/test_consumers.py
Normal file
240
awx/network_ui/tests/unit/test_consumers.py
Normal file
@ -0,0 +1,240 @@
|
||||
|
||||
import mock
|
||||
import logging
|
||||
import json
|
||||
import imp
|
||||
from mock import patch
|
||||
patch('channels.auth.channel_session_user', lambda x: x).start()
|
||||
patch('channels.auth.channel_session_user_from_http', lambda x: x).start()
|
||||
|
||||
from awx.network_ui.consumers import parse_inventory_id, networking_events_dispatcher, send_snapshot # noqa
|
||||
from awx.network_ui.models import Topology, Device, Link, Interface, TopologyInventory, Client # noqa
|
||||
import awx # noqa
|
||||
import awx.network_ui # noqa
|
||||
import awx.network_ui.consumers # noqa
|
||||
imp.reload(awx.network_ui.consumers)
|
||||
|
||||
|
||||
def test_parse_inventory_id():
|
||||
assert parse_inventory_id({}) is None
|
||||
assert parse_inventory_id({'inventory_id': ['1']}) == 1
|
||||
assert parse_inventory_id({'inventory_id': ['0']}) is None
|
||||
assert parse_inventory_id({'inventory_id': ['X']}) is None
|
||||
assert parse_inventory_id({'inventory_id': []}) is None
|
||||
assert parse_inventory_id({'inventory_id': 'x'}) is None
|
||||
assert parse_inventory_id({'inventory_id': '12345'}) == 1
|
||||
assert parse_inventory_id({'inventory_id': 1}) is None
|
||||
|
||||
|
||||
def test_network_events_handle_message_incomplete_message1():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
with mock.patch.object(logger, 'warning') as log_mock:
|
||||
networking_events_dispatcher.handle({})
|
||||
log_mock.assert_called_once_with(
|
||||
'Unsupported message %s: no topology', {})
|
||||
|
||||
|
||||
def test_network_events_handle_message_incomplete_message2():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
with mock.patch.object(logger, 'warning') as log_mock:
|
||||
networking_events_dispatcher.handle({'topology': [0]})
|
||||
log_mock.assert_called_once_with(
|
||||
'Unsupported message %s: no client', {'topology': [0]})
|
||||
|
||||
|
||||
def test_network_events_handle_message_incomplete_message3():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
with mock.patch.object(logger, 'warning') as log_mock:
|
||||
networking_events_dispatcher.handle({'topology': [1]})
|
||||
log_mock.assert_called_once_with(
|
||||
'Unsupported message %s: no client', {'topology': [1]})
|
||||
|
||||
|
||||
def test_network_events_handle_message_incomplete_message4():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
with mock.patch.object(logger, 'warning') as log_mock:
|
||||
networking_events_dispatcher.handle({'topology': 1, 'client': 1})
|
||||
log_mock.assert_called_once_with('Unsupported message %s: no data', {
|
||||
'client': 1, 'topology': 1})
|
||||
|
||||
|
||||
def test_network_events_handle_message_incomplete_message5():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
with mock.patch.object(logger, 'warning') as log_mock:
|
||||
message = ['DeviceCreate']
|
||||
networking_events_dispatcher.handle(
|
||||
{'topology': 1, 'client': 1, 'text': json.dumps(message)})
|
||||
log_mock.assert_called_once_with('Unsupported message %s: no message type', {
|
||||
'text': '["DeviceCreate"]', 'client': 1, 'topology': 1})
|
||||
|
||||
|
||||
def test_network_events_handle_message_incomplete_message6():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
with mock.patch.object(logger, 'warning') as log_mock:
|
||||
message = ['DeviceCreate', []]
|
||||
networking_events_dispatcher.handle(
|
||||
{'topology': 1, 'client': 1, 'text': json.dumps(message)})
|
||||
log_mock.assert_has_calls([
|
||||
mock.call('Message has no sender'),
|
||||
mock.call('Unsupported message %s: no message type', {'text': '["DeviceCreate", []]', 'client': 1, 'topology': 1})])
|
||||
|
||||
|
||||
def test_network_events_handle_message_incomplete_message7():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
with mock.patch.object(logger, 'warning') as log_mock:
|
||||
message = ['DeviceCreate', {}]
|
||||
networking_events_dispatcher.handle(
|
||||
{'topology': 1, 'client': 1, 'text': json.dumps(message)})
|
||||
log_mock.assert_has_calls([
|
||||
mock.call('client_id mismatch expected: %s actual %s', 1, None),
|
||||
mock.call('Unsupported message %s: no message type', {'text': '["DeviceCreate", {}]', 'client': 1, 'topology': 1})])
|
||||
|
||||
|
||||
def test_network_events_handle_message_incomplete_message8():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
with mock.patch.object(logger, 'warning') as log_mock:
|
||||
message = ['Unsupported', {'sender': 1}]
|
||||
networking_events_dispatcher.handle(
|
||||
{'topology': 1, 'client': 1, 'text': json.dumps(message)})
|
||||
log_mock.assert_called_once_with(
|
||||
'Unsupported message %s: no handler', u'Unsupported')
|
||||
|
||||
|
||||
def test_send_snapshot_empty():
|
||||
channel = mock.MagicMock()
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Device, 'objects'),\
|
||||
mock.patch.object(Link, 'objects'),\
|
||||
mock.patch.object(Interface, 'objects'),\
|
||||
mock.patch.object(Topology, 'objects'):
|
||||
send_snapshot(channel, 1)
|
||||
log_mock.assert_not_called()
|
||||
channel.send.assert_called_once_with(
|
||||
{'text': '["Snapshot", {"links": [], "devices": [], "sender": 0}]'})
|
||||
|
||||
|
||||
def test_send_snapshot_single():
|
||||
channel = mock.MagicMock()
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Device, 'objects') as device_objects_mock,\
|
||||
mock.patch.object(Link, 'objects'),\
|
||||
mock.patch.object(Interface, 'objects') as interface_objects_mock:
|
||||
|
||||
interface_objects_mock.filter.return_value.values.return_value = [
|
||||
dict(cid=1, device_id=1, id=1, name="eth0")]
|
||||
device_objects_mock.filter.return_value.values.return_value = [
|
||||
dict(cid=1, id=1, device_type="host", name="host1", x=0, y=0,
|
||||
interface_id_seq=1, host_id=1)]
|
||||
send_snapshot(channel, 1)
|
||||
device_objects_mock.filter.assert_called_once_with(topology_id=1)
|
||||
device_objects_mock.filter.return_value.values.assert_called_once_with()
|
||||
interface_objects_mock.filter.assert_called_once_with(
|
||||
device__topology_id=1)
|
||||
interface_objects_mock.filter.return_value.values.assert_called_once_with()
|
||||
log_mock.assert_not_called()
|
||||
channel.send.assert_called_once_with(
|
||||
{'text': '''["Snapshot", {"links": [], "devices": [{"interface_id_seq": 1, \
|
||||
"name": "host1", "interfaces": [{"id": 1, "device_id": 1, "name": "eth0", "interface_id": 1}], \
|
||||
"device_type": "host", "host_id": 1, "y": 0, "x": 0, "id": 1, "device_id": 1}], "sender": 0}]'''})
|
||||
|
||||
|
||||
def test_ws_disconnect():
|
||||
message = mock.MagicMock()
|
||||
message.channel_session = dict(topology_id=1)
|
||||
message.reply_channel = 'foo'
|
||||
with mock.patch('channels.Group') as group_mock:
|
||||
awx.network_ui.consumers.ws_disconnect(message)
|
||||
group_mock.assert_called_once_with('topology-1')
|
||||
group_mock.return_value.discard.assert_called_once_with('foo')
|
||||
|
||||
|
||||
def test_ws_disconnect_no_topology():
|
||||
message = mock.MagicMock()
|
||||
with mock.patch('channels.Group') as group_mock:
|
||||
awx.network_ui.consumers.ws_disconnect(message)
|
||||
group_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_ws_message():
|
||||
message = mock.MagicMock()
|
||||
message.channel_session = dict(topology_id=1, client_id=1)
|
||||
message.__getitem__.return_value = json.dumps([])
|
||||
print (message['text'])
|
||||
with mock.patch('channels.Group') as group_mock:
|
||||
awx.network_ui.consumers.ws_message(message)
|
||||
group_mock.assert_called_once_with('topology-1')
|
||||
group_mock.return_value.send.assert_called_once_with({'text': '[]'})
|
||||
|
||||
|
||||
def test_ws_connect_unauthenticated():
|
||||
message = mock.MagicMock()
|
||||
message.user.is_authenticated.return_value = False
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
with mock.patch.object(logger, 'error') as log_mock:
|
||||
awx.network_ui.consumers.ws_connect(message)
|
||||
log_mock.assert_called_once_with('Request user is not authenticated to use websocket.')
|
||||
|
||||
|
||||
def test_ws_connect_new_topology():
|
||||
message = mock.MagicMock()
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
with mock.patch('awx.network_ui.consumers.Client') as client_mock,\
|
||||
mock.patch('awx.network_ui.consumers.Topology') as topology_mock,\
|
||||
mock.patch('channels.Group'),\
|
||||
mock.patch('awx.network_ui.consumers.send_snapshot') as send_snapshot_mock,\
|
||||
mock.patch.object(logger, 'warning'),\
|
||||
mock.patch.object(TopologyInventory, 'objects'),\
|
||||
mock.patch.object(TopologyInventory, 'save'),\
|
||||
mock.patch.object(Topology, 'save'),\
|
||||
mock.patch.object(Topology, 'objects'),\
|
||||
mock.patch.object(Device, 'objects'),\
|
||||
mock.patch.object(Link, 'objects'),\
|
||||
mock.patch.object(Interface, 'objects'):
|
||||
client_mock.return_value.pk = 777
|
||||
topology_mock.return_value = Topology(
|
||||
name="topology", scale=1.0, panX=0, panY=0, pk=999)
|
||||
awx.network_ui.consumers.ws_connect(message)
|
||||
message.reply_channel.send.assert_has_calls([
|
||||
mock.call({'text': '["id", 777]'}),
|
||||
mock.call({'text': '["topology_id", 999]'}),
|
||||
mock.call(
|
||||
{'text': '["Topology", {"scale": 1.0, "name": "topology", "device_id_seq": 0, "panY": 0, "panX": 0, "topology_id": 999, "link_id_seq": 0}]'}),
|
||||
])
|
||||
send_snapshot_mock.assert_called_once_with(message.reply_channel, 999)
|
||||
|
||||
|
||||
def test_ws_connect_existing_topology():
|
||||
message = mock.MagicMock()
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
with mock.patch('awx.network_ui.consumers.Client') as client_mock,\
|
||||
mock.patch('awx.network_ui.consumers.send_snapshot') as send_snapshot_mock,\
|
||||
mock.patch('channels.Group'),\
|
||||
mock.patch.object(logger, 'warning'),\
|
||||
mock.patch.object(TopologyInventory, 'objects') as topology_inventory_objects_mock,\
|
||||
mock.patch.object(TopologyInventory, 'save'),\
|
||||
mock.patch.object(Topology, 'save'),\
|
||||
mock.patch.object(Topology, 'objects') as topology_objects_mock,\
|
||||
mock.patch.object(Device, 'objects'),\
|
||||
mock.patch.object(Link, 'objects'),\
|
||||
mock.patch.object(Interface, 'objects'):
|
||||
topology_inventory_objects_mock.filter.return_value.values_list.return_value = [
|
||||
1]
|
||||
client_mock.return_value.pk = 888
|
||||
topology_objects_mock.get.return_value = Topology(pk=1001,
|
||||
id=1,
|
||||
name="topo",
|
||||
panX=0,
|
||||
panY=0,
|
||||
scale=1.0,
|
||||
link_id_seq=1,
|
||||
device_id_seq=1)
|
||||
awx.network_ui.consumers.ws_connect(message)
|
||||
message.reply_channel.send.assert_has_calls([
|
||||
mock.call({'text': '["id", 888]'}),
|
||||
mock.call({'text': '["topology_id", 1001]'}),
|
||||
mock.call(
|
||||
{'text': '["Topology", {"scale": 1.0, "name": "topo", "device_id_seq": 1, "panY": 0, "panX": 0, "topology_id": 1001, "link_id_seq": 1}]'}),
|
||||
])
|
||||
send_snapshot_mock.assert_called_once_with(message.reply_channel, 1001)
|
||||
15
awx/network_ui/tests/unit/test_models.py
Normal file
15
awx/network_ui/tests/unit/test_models.py
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
from awx.network_ui.models import Device, Topology, Interface
|
||||
|
||||
|
||||
def test_device():
|
||||
assert str(Device(name="foo")) == "foo"
|
||||
|
||||
|
||||
def test_topology():
|
||||
assert str(Topology(name="foo")) == "foo"
|
||||
|
||||
|
||||
def test_interface():
|
||||
assert str(Interface(name="foo")) == "foo"
|
||||
451
awx/network_ui/tests/unit/test_network_events.py
Normal file
451
awx/network_ui/tests/unit/test_network_events.py
Normal file
@ -0,0 +1,451 @@
|
||||
import mock
|
||||
import json
|
||||
import logging
|
||||
|
||||
from awx.network_ui.consumers import networking_events_dispatcher
|
||||
from awx.network_ui.models import Topology, Device, Link, Interface
|
||||
|
||||
|
||||
def message(message):
|
||||
def wrapper(fn):
|
||||
fn.tests_message = message
|
||||
return fn
|
||||
return wrapper
|
||||
|
||||
|
||||
@message('DeviceMove')
|
||||
def test_network_events_handle_message_DeviceMove():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['DeviceMove', dict(
|
||||
msg_type='DeviceMove',
|
||||
sender=1,
|
||||
id=1,
|
||||
x=100,
|
||||
y=100,
|
||||
previous_x=0,
|
||||
previous_y=0
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Device, 'objects') as device_objects_mock:
|
||||
networking_events_dispatcher.handle(message)
|
||||
device_objects_mock.filter.assert_called_once_with(
|
||||
cid=1, topology_id=1)
|
||||
device_objects_mock.filter.return_value.update.assert_called_once_with(
|
||||
x=100, y=100)
|
||||
log_mock.assert_not_called()
|
||||
|
||||
|
||||
@message('DeviceCreate')
|
||||
def test_network_events_handle_message_DeviceCreate():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['DeviceCreate', dict(msg_type='DeviceCreate',
|
||||
sender=1,
|
||||
id=1,
|
||||
x=0,
|
||||
y=0,
|
||||
name="test_created",
|
||||
type='host',
|
||||
host_id=None)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Topology.objects, 'filter') as topology_objects_mock,\
|
||||
mock.patch.object(Device.objects, 'get_or_create') as device_objects_mock:
|
||||
device_mock = mock.MagicMock()
|
||||
filter_mock = mock.MagicMock()
|
||||
device_objects_mock.return_value = [device_mock, True]
|
||||
topology_objects_mock.return_value = filter_mock
|
||||
networking_events_dispatcher.handle(message)
|
||||
device_objects_mock.assert_called_once_with(
|
||||
cid=1,
|
||||
defaults={'name': u'test_created', 'cid': 1, 'device_type': u'host',
|
||||
'x': 0, 'y': 0, 'host_id': None},
|
||||
topology_id=1)
|
||||
device_mock.save.assert_called_once_with()
|
||||
topology_objects_mock.assert_called_once_with(
|
||||
device_id_seq__lt=1, pk=1)
|
||||
filter_mock.update.assert_called_once_with(device_id_seq=1)
|
||||
log_mock.assert_not_called()
|
||||
|
||||
|
||||
@message('DeviceLabelEdit')
|
||||
def test_network_events_handle_message_DeviceLabelEdit():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['DeviceLabelEdit', dict(
|
||||
msg_type='DeviceLabelEdit',
|
||||
sender=1,
|
||||
id=1,
|
||||
name='test_changed',
|
||||
previous_name='test_created'
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Device.objects, 'filter') as device_objects_filter_mock:
|
||||
networking_events_dispatcher.handle(message)
|
||||
device_objects_filter_mock.assert_called_once_with(
|
||||
cid=1, topology_id=1)
|
||||
log_mock.assert_not_called()
|
||||
|
||||
|
||||
@message('DeviceSelected')
|
||||
def test_network_events_handle_message_DeviceSelected():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['DeviceSelected', dict(
|
||||
msg_type='DeviceSelected',
|
||||
sender=1,
|
||||
id=1
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock:
|
||||
networking_events_dispatcher.handle(message)
|
||||
log_mock.assert_not_called()
|
||||
|
||||
|
||||
@message('DeviceUnSelected')
|
||||
def test_network_events_handle_message_DeviceUnSelected():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['DeviceUnSelected', dict(
|
||||
msg_type='DeviceUnSelected',
|
||||
sender=1,
|
||||
id=1
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock:
|
||||
networking_events_dispatcher.handle(message)
|
||||
log_mock.assert_not_called()
|
||||
|
||||
|
||||
@message('DeviceDestroy')
|
||||
def test_network_events_handle_message_DeviceDestory():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['DeviceDestroy', dict(
|
||||
msg_type='DeviceDestroy',
|
||||
sender=1,
|
||||
id=1,
|
||||
previous_x=0,
|
||||
previous_y=0,
|
||||
previous_name="",
|
||||
previous_type="host",
|
||||
previous_host_id="1")]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Device, 'objects') as device_objects_mock:
|
||||
networking_events_dispatcher.handle(message)
|
||||
device_objects_mock.filter.assert_called_once_with(
|
||||
cid=1, topology_id=1)
|
||||
device_objects_mock.filter.return_value.delete.assert_called_once_with()
|
||||
log_mock.assert_not_called()
|
||||
|
||||
|
||||
@message('InterfaceCreate')
|
||||
def test_network_events_handle_message_InterfaceCreate():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['InterfaceCreate', dict(
|
||||
msg_type='InterfaceCreate',
|
||||
sender=1,
|
||||
device_id=1,
|
||||
id=1,
|
||||
name='eth0'
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Device, 'objects') as device_objects_mock,\
|
||||
mock.patch.object(Interface, 'objects') as interface_objects_mock:
|
||||
device_objects_mock.get.return_value.pk = 99
|
||||
networking_events_dispatcher.handle(message)
|
||||
device_objects_mock.get.assert_called_once_with(cid=1, topology_id=1)
|
||||
device_objects_mock.filter.assert_called_once_with(
|
||||
cid=1, interface_id_seq__lt=1, topology_id=1)
|
||||
interface_objects_mock.get_or_create.assert_called_once_with(
|
||||
cid=1, defaults={'name': u'eth0'}, device_id=99)
|
||||
log_mock.assert_not_called()
|
||||
|
||||
|
||||
@message('InterfaceLabelEdit')
|
||||
def test_network_events_handle_message_InterfaceLabelEdit():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['InterfaceLabelEdit', dict(
|
||||
msg_type='InterfaceLabelEdit',
|
||||
sender=1,
|
||||
id=1,
|
||||
device_id=1,
|
||||
name='new name',
|
||||
previous_name='old name'
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Interface, 'objects') as interface_objects_mock:
|
||||
networking_events_dispatcher.handle(message)
|
||||
interface_objects_mock.filter.assert_called_once_with(
|
||||
cid=1, device__cid=1, device__topology_id=1)
|
||||
interface_objects_mock.filter.return_value.update.assert_called_once_with(
|
||||
name=u'new name')
|
||||
log_mock.assert_not_called()
|
||||
|
||||
|
||||
@message('LinkLabelEdit')
|
||||
def test_network_events_handle_message_LinkLabelEdit():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['LinkLabelEdit', dict(
|
||||
msg_type='LinkLabelEdit',
|
||||
sender=1,
|
||||
id=1,
|
||||
name='new name',
|
||||
previous_name='old name'
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Link, 'objects') as link_objects_mock:
|
||||
networking_events_dispatcher.handle(message)
|
||||
link_objects_mock.filter.assert_called_once_with(
|
||||
cid=1, from_device__topology_id=1)
|
||||
link_objects_mock.filter.return_value.update.assert_called_once_with(
|
||||
name=u'new name')
|
||||
log_mock.assert_not_called()
|
||||
|
||||
|
||||
@message('LinkCreate')
|
||||
def test_network_events_handle_message_LinkCreate():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['LinkCreate', dict(
|
||||
msg_type='LinkCreate',
|
||||
id=1,
|
||||
sender=1,
|
||||
name="",
|
||||
from_device_id=1,
|
||||
to_device_id=2,
|
||||
from_interface_id=1,
|
||||
to_interface_id=1
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Device, 'objects') as device_objects_mock,\
|
||||
mock.patch.object(Link, 'objects') as link_objects_mock,\
|
||||
mock.patch.object(Interface, 'objects') as interface_objects_mock,\
|
||||
mock.patch.object(Topology, 'objects') as topology_objects_mock:
|
||||
values_list_mock = mock.MagicMock()
|
||||
values_list_mock.values_list.return_value = [(1,1), (2,2)]
|
||||
interface_objects_mock.get.return_value = mock.MagicMock()
|
||||
interface_objects_mock.get.return_value.pk = 7
|
||||
device_objects_mock.filter.return_value = values_list_mock
|
||||
topology_objects_mock.filter.return_value = mock.MagicMock()
|
||||
networking_events_dispatcher.handle(message)
|
||||
device_objects_mock.filter.assert_called_once_with(
|
||||
cid__in=[1, 2], topology_id=1)
|
||||
values_list_mock.values_list.assert_called_once_with('cid', 'pk')
|
||||
link_objects_mock.get_or_create.assert_called_once_with(
|
||||
cid=1, from_device_id=1, from_interface_id=7, name=u'',
|
||||
to_device_id=2, to_interface_id=7)
|
||||
topology_objects_mock.filter.assert_called_once_with(
|
||||
link_id_seq__lt=1, pk=1)
|
||||
topology_objects_mock.filter.return_value.update.assert_called_once_with(
|
||||
link_id_seq=1)
|
||||
log_mock.assert_not_called()
|
||||
|
||||
|
||||
@message('LinkCreate')
|
||||
def test_network_events_handle_message_LinkCreate_bad_device1():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['LinkCreate', dict(
|
||||
msg_type='LinkCreate',
|
||||
id=1,
|
||||
sender=1,
|
||||
name="",
|
||||
from_device_id=1,
|
||||
to_device_id=2,
|
||||
from_interface_id=1,
|
||||
to_interface_id=1
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Device, 'objects') as device_objects_mock,\
|
||||
mock.patch.object(Link, 'objects'),\
|
||||
mock.patch.object(Interface, 'objects') as interface_objects_mock,\
|
||||
mock.patch.object(Topology, 'objects') as topology_objects_mock:
|
||||
values_list_mock = mock.MagicMock()
|
||||
values_list_mock.values_list.return_value = [(9,1), (2,2)]
|
||||
interface_objects_mock.get.return_value = mock.MagicMock()
|
||||
interface_objects_mock.get.return_value.pk = 7
|
||||
device_objects_mock.filter.return_value = values_list_mock
|
||||
topology_objects_mock.filter.return_value = mock.MagicMock()
|
||||
networking_events_dispatcher.handle(message)
|
||||
device_objects_mock.filter.assert_called_once_with(
|
||||
cid__in=[1, 2], topology_id=1)
|
||||
values_list_mock.values_list.assert_called_once_with('cid', 'pk')
|
||||
log_mock.assert_called_once_with('Device not found')
|
||||
|
||||
|
||||
@message('LinkCreate')
|
||||
def test_network_events_handle_message_LinkCreate_bad_device2():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['LinkCreate', dict(
|
||||
msg_type='LinkCreate',
|
||||
id=1,
|
||||
sender=1,
|
||||
name="",
|
||||
from_device_id=1,
|
||||
to_device_id=2,
|
||||
from_interface_id=1,
|
||||
to_interface_id=1
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Device, 'objects') as device_objects_mock,\
|
||||
mock.patch.object(Link, 'objects'),\
|
||||
mock.patch.object(Interface, 'objects') as interface_objects_mock,\
|
||||
mock.patch.object(Topology, 'objects') as topology_objects_mock:
|
||||
values_list_mock = mock.MagicMock()
|
||||
values_list_mock.values_list.return_value = [(1,1), (9,2)]
|
||||
interface_objects_mock.get.return_value = mock.MagicMock()
|
||||
interface_objects_mock.get.return_value.pk = 7
|
||||
device_objects_mock.filter.return_value = values_list_mock
|
||||
topology_objects_mock.filter.return_value = mock.MagicMock()
|
||||
networking_events_dispatcher.handle(message)
|
||||
device_objects_mock.filter.assert_called_once_with(
|
||||
cid__in=[1, 2], topology_id=1)
|
||||
values_list_mock.values_list.assert_called_once_with('cid', 'pk')
|
||||
log_mock.assert_called_once_with('Device not found')
|
||||
|
||||
|
||||
@message('LinkDestroy')
|
||||
def test_network_events_handle_message_LinkDestroy():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['LinkDestroy', dict(
|
||||
msg_type='LinkDestroy',
|
||||
id=1,
|
||||
sender=1,
|
||||
name="",
|
||||
from_device_id=1,
|
||||
to_device_id=2,
|
||||
from_interface_id=1,
|
||||
to_interface_id=1
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Device.objects, 'filter') as device_filter_mock,\
|
||||
mock.patch.object(Link.objects, 'filter') as link_filter_mock,\
|
||||
mock.patch.object(Interface.objects, 'get') as interface_get_mock:
|
||||
values_mock = mock.MagicMock()
|
||||
interface_get_mock.return_value = mock.MagicMock()
|
||||
interface_get_mock.return_value.pk = 7
|
||||
device_filter_mock.return_value = values_mock
|
||||
values_mock.values_list.return_value = [(1,1), (2,2)]
|
||||
networking_events_dispatcher.handle(message)
|
||||
device_filter_mock.assert_called_once_with(
|
||||
cid__in=[1, 2], topology_id=1)
|
||||
values_mock.values_list.assert_called_once_with('cid', 'pk')
|
||||
link_filter_mock.assert_called_once_with(
|
||||
cid=1, from_device_id=1, from_interface_id=7, to_device_id=2, to_interface_id=7)
|
||||
log_mock.assert_not_called()
|
||||
|
||||
|
||||
@message('LinkDestroy')
|
||||
def test_network_events_handle_message_LinkDestroy_bad_device_map1():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['LinkDestroy', dict(
|
||||
msg_type='LinkDestroy',
|
||||
id=1,
|
||||
sender=1,
|
||||
name="",
|
||||
from_device_id=1,
|
||||
to_device_id=2,
|
||||
from_interface_id=1,
|
||||
to_interface_id=1
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Device.objects, 'filter') as device_filter_mock,\
|
||||
mock.patch.object(Link.objects, 'filter'),\
|
||||
mock.patch.object(Interface.objects, 'get') as interface_get_mock:
|
||||
values_mock = mock.MagicMock()
|
||||
interface_get_mock.return_value = mock.MagicMock()
|
||||
interface_get_mock.return_value.pk = 7
|
||||
device_filter_mock.return_value = values_mock
|
||||
values_mock.values_list.return_value = [(9,1), (2,2)]
|
||||
networking_events_dispatcher.handle(message)
|
||||
log_mock.assert_called_once_with('Device not found')
|
||||
|
||||
|
||||
@message('LinkDestroy')
|
||||
def test_network_events_handle_message_LinkDestroy_bad_device_map2():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['LinkDestroy', dict(
|
||||
msg_type='LinkDestroy',
|
||||
id=1,
|
||||
sender=1,
|
||||
name="",
|
||||
from_device_id=1,
|
||||
to_device_id=2,
|
||||
from_interface_id=1,
|
||||
to_interface_id=1
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock,\
|
||||
mock.patch.object(Device.objects, 'filter') as device_filter_mock,\
|
||||
mock.patch.object(Link.objects, 'filter'),\
|
||||
mock.patch.object(Interface.objects, 'get') as interface_get_mock:
|
||||
values_mock = mock.MagicMock()
|
||||
interface_get_mock.return_value = mock.MagicMock()
|
||||
interface_get_mock.return_value.pk = 7
|
||||
device_filter_mock.return_value = values_mock
|
||||
values_mock.values_list.return_value = [(1,1), (9,2)]
|
||||
networking_events_dispatcher.handle(message)
|
||||
log_mock.assert_called_once_with('Device not found')
|
||||
|
||||
|
||||
@message('LinkSelected')
|
||||
def test_network_events_handle_message_LinkSelected():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['LinkSelected', dict(
|
||||
msg_type='LinkSelected',
|
||||
sender=1,
|
||||
id=1
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock:
|
||||
networking_events_dispatcher.handle(message)
|
||||
log_mock.assert_not_called()
|
||||
|
||||
|
||||
@message('LinkUnSelected')
|
||||
def test_network_events_handle_message_LinkUnSelected():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['LinkUnSelected', dict(
|
||||
msg_type='LinkUnSelected',
|
||||
sender=1,
|
||||
id=1
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock:
|
||||
networking_events_dispatcher.handle(message)
|
||||
log_mock.assert_not_called()
|
||||
|
||||
|
||||
@message('MultipleMessage')
|
||||
def test_network_events_handle_message_MultipleMessage_unsupported_message():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['MultipleMessage', dict(
|
||||
msg_type='MultipleMessage',
|
||||
sender=1,
|
||||
messages=[dict(msg_type="Unsupported")]
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock:
|
||||
networking_events_dispatcher.handle(message)
|
||||
log_mock.assert_called_once_with(
|
||||
'Unsupported message %s', u'Unsupported')
|
||||
|
||||
|
||||
@message('MultipleMessage')
|
||||
def test_network_events_handle_message_MultipleMessage():
|
||||
logger = logging.getLogger('awx.network_ui.consumers')
|
||||
message_data = ['MultipleMessage', dict(
|
||||
msg_type='MultipleMessage',
|
||||
sender=1,
|
||||
messages=[dict(msg_type="DeviceSelected")]
|
||||
)]
|
||||
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
|
||||
with mock.patch.object(logger, 'warning') as log_mock:
|
||||
networking_events_dispatcher.handle(message)
|
||||
log_mock.assert_not_called()
|
||||
9
awx/network_ui/tests/unit/test_routing.py
Normal file
9
awx/network_ui/tests/unit/test_routing.py
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
import awx.network_ui.routing
|
||||
|
||||
|
||||
def test_routing():
|
||||
'''
|
||||
Tests that the number of routes in awx.network_ui.routing is 3.
|
||||
'''
|
||||
assert len(awx.network_ui.routing.channel_routing) == 3
|
||||
65
awx/network_ui/tests/unit/test_views.py
Normal file
65
awx/network_ui/tests/unit/test_views.py
Normal file
@ -0,0 +1,65 @@
|
||||
|
||||
import mock
|
||||
|
||||
from awx.network_ui.views import topology_data, NetworkAnnotatedInterface, json_topology_data, yaml_topology_data
|
||||
from awx.network_ui.models import Topology, Device, Link, Interface
|
||||
|
||||
|
||||
|
||||
def test_topology_data():
|
||||
with mock.patch.object(Topology, 'objects'),\
|
||||
mock.patch.object(Device, 'objects') as device_objects_mock,\
|
||||
mock.patch.object(Link, 'objects') as link_objects_mock,\
|
||||
mock.patch.object(Interface, 'objects'),\
|
||||
mock.patch.object(NetworkAnnotatedInterface, 'filter'):
|
||||
device_objects_mock.filter.return_value.order_by.return_value = [
|
||||
Device(pk=1), Device(pk=2)]
|
||||
link_objects_mock.filter.return_value = [Link(from_device=Device(name='from', cid=1),
|
||||
to_device=Device(
|
||||
name='to', cid=2),
|
||||
from_interface=Interface(
|
||||
name="eth0", cid=1),
|
||||
to_interface=Interface(
|
||||
name="eth0", cid=1),
|
||||
name="",
|
||||
pk=1
|
||||
)]
|
||||
data = topology_data(1)
|
||||
assert len(data['devices']) == 2
|
||||
assert len(data['links']) == 1
|
||||
|
||||
|
||||
def test_json_topology_data():
|
||||
request = mock.MagicMock()
|
||||
request.GET = dict(topology_id=1)
|
||||
with mock.patch('awx.network_ui.views.topology_data') as topology_data_mock:
|
||||
topology_data_mock.return_value = dict()
|
||||
json_topology_data(request)
|
||||
topology_data_mock.assert_called_once_with(1)
|
||||
|
||||
|
||||
def test_yaml_topology_data():
|
||||
request = mock.MagicMock()
|
||||
request.GET = dict(topology_id=1)
|
||||
with mock.patch('awx.network_ui.views.topology_data') as topology_data_mock:
|
||||
topology_data_mock.return_value = dict()
|
||||
yaml_topology_data(request)
|
||||
topology_data_mock.assert_called_once_with(1)
|
||||
|
||||
|
||||
def test_json_topology_data_no_topology_id():
|
||||
request = mock.MagicMock()
|
||||
request.GET = dict()
|
||||
with mock.patch('awx.network_ui.views.topology_data') as topology_data_mock:
|
||||
topology_data_mock.return_value = dict()
|
||||
json_topology_data(request)
|
||||
topology_data_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_yaml_topology_data_no_topology_id():
|
||||
request = mock.MagicMock()
|
||||
request.GET = dict()
|
||||
with mock.patch('awx.network_ui.views.topology_data') as topology_data_mock:
|
||||
topology_data_mock.return_value = dict()
|
||||
yaml_topology_data(request)
|
||||
topology_data_mock.assert_not_called()
|
||||
@ -5,6 +5,6 @@ from awx.network_ui import views
|
||||
|
||||
app_name = 'network_ui'
|
||||
urlpatterns = [
|
||||
url(r'^topology.json$', views.json_topology_data, name='json_topology_data'),
|
||||
url(r'^topology.yaml$', views.yaml_topology_data, name='yaml_topology_data'),
|
||||
url(r'^topology.json/?$', views.json_topology_data, name='json_topology_data'),
|
||||
url(r'^topology.yaml/?$', views.yaml_topology_data, name='yaml_topology_data'),
|
||||
]
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
# Copyright (c) 2017 Red Hat, Inc
|
||||
from django.shortcuts import render
|
||||
from django import forms
|
||||
from django.http import JsonResponse, HttpResponseBadRequest, HttpResponse
|
||||
from awx.network_ui.models import Topology, Device, Link, Interface
|
||||
from django.db.models import Q
|
||||
import yaml
|
||||
import json
|
||||
|
||||
NetworkAnnotatedInterface = Interface.objects.values('name',
|
||||
'cid',
|
||||
@ -63,18 +61,6 @@ def topology_data(topology_id):
|
||||
return data
|
||||
|
||||
|
||||
def yaml_serialize_topology(topology_id):
|
||||
return yaml.safe_dump(topology_data(topology_id), default_flow_style=False)
|
||||
|
||||
|
||||
def json_serialize_topology(topology_id):
|
||||
return json.dumps(topology_data(topology_id))
|
||||
|
||||
|
||||
def index(request):
|
||||
return render(request, "network_ui/index.html", dict(topologies=Topology.objects.all().order_by('-pk')))
|
||||
|
||||
|
||||
class TopologyForm(forms.Form):
|
||||
topology_id = forms.IntegerField()
|
||||
|
||||
@ -82,7 +68,10 @@ class TopologyForm(forms.Form):
|
||||
def json_topology_data(request):
|
||||
form = TopologyForm(request.GET)
|
||||
if form.is_valid():
|
||||
return JsonResponse(topology_data(form.cleaned_data['topology_id']))
|
||||
response = JsonResponse(topology_data(form.cleaned_data['topology_id']),
|
||||
content_type='application/force-download')
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format('topology.json')
|
||||
return response
|
||||
else:
|
||||
return HttpResponseBadRequest(form.errors)
|
||||
|
||||
@ -90,9 +79,11 @@ def json_topology_data(request):
|
||||
def yaml_topology_data(request):
|
||||
form = TopologyForm(request.GET)
|
||||
if form.is_valid():
|
||||
return HttpResponse(yaml.safe_dump(topology_data(form.cleaned_data['topology_id']),
|
||||
default_flow_style=False),
|
||||
content_type='application/yaml')
|
||||
response = HttpResponse(yaml.safe_dump(topology_data(form.cleaned_data['topology_id']),
|
||||
default_flow_style=False),
|
||||
content_type='application/force-download')
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format('topology.yaml')
|
||||
return response
|
||||
else:
|
||||
return HttpResponseBadRequest(form.errors)
|
||||
|
||||
|
||||
@ -90,6 +90,7 @@ def read_tower_inventory(tower_host, tower_user, tower_pass, inventory, license_
|
||||
tower_host = "https://{}".format(tower_host)
|
||||
inventory_url = urljoin(tower_host, "/api/v2/inventories/{}/script/?hostvars=1&towervars=1&all=1".format(inventory.replace('/', '')))
|
||||
config_url = urljoin(tower_host, "/api/v2/config/")
|
||||
reason = None
|
||||
try:
|
||||
if license_type != "open":
|
||||
config_response = requests.get(config_url,
|
||||
@ -106,14 +107,16 @@ def read_tower_inventory(tower_host, tower_user, tower_pass, inventory, license_
|
||||
response = requests.get(inventory_url,
|
||||
auth=HTTPBasicAuth(tower_user, tower_pass),
|
||||
verify=not ignore_ssl)
|
||||
try:
|
||||
json_response = response.json()
|
||||
except (ValueError, TypeError) as e:
|
||||
reason = "Failed to parse json from host: {}".format(e)
|
||||
if response.ok:
|
||||
return response.json()
|
||||
json_reason = response.json()
|
||||
reason = json_reason.get('detail', 'Retrieving Tower Inventory Failed')
|
||||
return json_response
|
||||
if not reason:
|
||||
reason = json_response.get('detail', 'Retrieving Tower Inventory Failed')
|
||||
except requests.ConnectionError as e:
|
||||
reason = "Connection to remote host failed: {}".format(e)
|
||||
except json.JSONDecodeError as e:
|
||||
reason = "Failed to parse json from host: {}".format(e)
|
||||
raise RuntimeError(reason)
|
||||
|
||||
|
||||
|
||||
@ -4,13 +4,10 @@
|
||||
import os
|
||||
import re # noqa
|
||||
import sys
|
||||
import ldap
|
||||
import djcelery
|
||||
import six
|
||||
from datetime import timedelta
|
||||
|
||||
from kombu.common import Broadcast
|
||||
|
||||
# global settings
|
||||
from django.conf import global_settings
|
||||
# ugettext lazy
|
||||
@ -41,6 +38,13 @@ def IS_TESTING(argv=None):
|
||||
return is_testing(argv)
|
||||
|
||||
|
||||
if "pytest" in sys.modules:
|
||||
import mock
|
||||
with mock.patch('__main__.__builtins__.dir', return_value=[]):
|
||||
import ldap
|
||||
else:
|
||||
import ldap
|
||||
|
||||
DEBUG = True
|
||||
SQL_DEBUG = DEBUG
|
||||
|
||||
@ -456,6 +460,9 @@ BROKER_POOL_LIMIT = None
|
||||
BROKER_URL = 'amqp://guest:guest@localhost:5672//'
|
||||
CELERY_EVENT_QUEUE_TTL = 5
|
||||
CELERY_DEFAULT_QUEUE = 'awx_private_queue'
|
||||
CELERY_DEFAULT_EXCHANGE = 'awx_private_queue'
|
||||
CELERY_DEFAULT_ROUTING_KEY = 'awx_private_queue'
|
||||
CELERY_DEFAULT_EXCHANGE_TYPE = 'direct'
|
||||
CELERY_TASK_SERIALIZER = 'json'
|
||||
CELERY_RESULT_SERIALIZER = 'json'
|
||||
CELERY_ACCEPT_CONTENT = ['json']
|
||||
@ -466,10 +473,8 @@ CELERYD_POOL_RESTARTS = True
|
||||
CELERYD_AUTOSCALER = 'awx.main.utils.autoscale:DynamicAutoScaler'
|
||||
CELERY_RESULT_BACKEND = 'djcelery.backends.database:DatabaseBackend'
|
||||
CELERY_IMPORTS = ('awx.main.scheduler.tasks',)
|
||||
CELERY_QUEUES = (
|
||||
Broadcast('tower_broadcast_all'),
|
||||
)
|
||||
CELERY_ROUTES = {}
|
||||
CELERY_QUEUES = ()
|
||||
CELERY_ROUTES = ('awx.main.utils.ha.AWXCeleryRouter',)
|
||||
|
||||
|
||||
def log_celery_failure(*args):
|
||||
@ -532,19 +537,12 @@ ASGI_AMQP = {
|
||||
}
|
||||
|
||||
# Django Caching Configuration
|
||||
if is_testing():
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
},
|
||||
}
|
||||
else:
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
||||
'LOCATION': 'memcached:11211',
|
||||
},
|
||||
}
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
||||
'LOCATION': 'memcached:11211',
|
||||
},
|
||||
}
|
||||
|
||||
# Social Auth configuration.
|
||||
SOCIAL_AUTH_STRATEGY = 'social_django.strategy.DjangoStrategy'
|
||||
@ -1005,6 +1003,9 @@ LOGGING = {
|
||||
'require_debug_true_or_test': {
|
||||
'()': 'awx.main.utils.RequireDebugTrueOrTest',
|
||||
},
|
||||
'external_log_enabled': {
|
||||
'()': 'awx.main.utils.filters.ExternalLoggerEnabled'
|
||||
},
|
||||
},
|
||||
'formatters': {
|
||||
'simple': {
|
||||
@ -1038,11 +1039,10 @@ LOGGING = {
|
||||
'class': 'logging.NullHandler',
|
||||
'formatter': 'simple',
|
||||
},
|
||||
'http_receiver': {
|
||||
'class': 'awx.main.utils.handlers.HTTPSNullHandler',
|
||||
'level': 'DEBUG',
|
||||
'external_logger': {
|
||||
'class': 'awx.main.utils.handlers.AWXProxyHandler',
|
||||
'formatter': 'json',
|
||||
'host': '',
|
||||
'filters': ['external_log_enabled'],
|
||||
},
|
||||
'mail_admins': {
|
||||
'level': 'ERROR',
|
||||
@ -1135,7 +1135,7 @@ LOGGING = {
|
||||
'handlers': ['console'],
|
||||
},
|
||||
'awx': {
|
||||
'handlers': ['console', 'file', 'tower_warnings'],
|
||||
'handlers': ['console', 'file', 'tower_warnings', 'external_logger'],
|
||||
'level': 'DEBUG',
|
||||
},
|
||||
'awx.conf': {
|
||||
@ -1160,16 +1160,13 @@ LOGGING = {
|
||||
'propagate': False
|
||||
},
|
||||
'awx.main.tasks': {
|
||||
'handlers': ['task_system'],
|
||||
'handlers': ['task_system', 'external_logger'],
|
||||
'propagate': False
|
||||
},
|
||||
'awx.main.scheduler': {
|
||||
'handlers': ['task_system'],
|
||||
'handlers': ['task_system', 'external_logger'],
|
||||
'propagate': False
|
||||
},
|
||||
'awx.main.consumers': {
|
||||
'handlers': ['null']
|
||||
},
|
||||
'awx.main.access': {
|
||||
'handlers': ['null'],
|
||||
'propagate': False,
|
||||
@ -1183,7 +1180,7 @@ LOGGING = {
|
||||
'propagate': False,
|
||||
},
|
||||
'awx.analytics': {
|
||||
'handlers': ['http_receiver'],
|
||||
'handlers': ['external_logger'],
|
||||
'level': 'INFO',
|
||||
'propagate': False
|
||||
},
|
||||
|
||||
@ -9,6 +9,7 @@ import socket
|
||||
import copy
|
||||
import sys
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
# Centos-7 doesn't include the svg mime type
|
||||
# /usr/lib64/python/mimetypes.py
|
||||
@ -20,6 +21,15 @@ from split_settings.tools import optional, include
|
||||
# Load default settings.
|
||||
from defaults import * # NOQA
|
||||
|
||||
# don't use memcache when running tests
|
||||
if "pytest" in sys.modules:
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'LOCATION': 'unique-{}'.format(str(uuid.uuid4())),
|
||||
},
|
||||
}
|
||||
|
||||
# awx-manage shell_plus --notebook
|
||||
NOTEBOOK_ARGUMENTS = [
|
||||
'--NotebookApp.token=',
|
||||
@ -103,13 +113,6 @@ if 'django_jenkins' in INSTALLED_APPS:
|
||||
|
||||
INSTALLED_APPS += ('rest_framework_swagger',)
|
||||
|
||||
# Much faster than the default
|
||||
# https://docs.djangoproject.com/en/1.6/topics/auth/passwords/#how-django-stores-passwords
|
||||
PASSWORD_HASHERS = (
|
||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||
)
|
||||
|
||||
# Configure a default UUID for development only.
|
||||
SYSTEM_UUID = '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
@ -149,8 +152,6 @@ SERVICE_NAME_DICT = {
|
||||
"uwsgi": "uwsgi",
|
||||
"daphne": "daphne",
|
||||
"nginx": "nginx"}
|
||||
# Used for sending commands in automatic restart
|
||||
UWSGI_FIFO_LOCATION = '/awxfifo'
|
||||
|
||||
try:
|
||||
socket.gethostbyname('docker.for.mac.internal')
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
###############################################################################
|
||||
import os
|
||||
import urllib
|
||||
import sys
|
||||
|
||||
|
||||
def patch_broken_pipe_error():
|
||||
@ -66,7 +67,7 @@ DATABASES = {
|
||||
# Use SQLite for unit tests instead of PostgreSQL. If the lines below are
|
||||
# commented out, Django will create the test_awx-dev database in PostgreSQL to
|
||||
# run unit tests.
|
||||
if is_testing(sys.argv):
|
||||
if "pytest" in sys.modules:
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
@ -195,11 +196,10 @@ LOGGING['handlers']['syslog'] = {
|
||||
|
||||
LOGGING['loggers']['django.request']['handlers'] = ['console']
|
||||
LOGGING['loggers']['rest_framework.request']['handlers'] = ['console']
|
||||
LOGGING['loggers']['awx']['handlers'] = ['console']
|
||||
LOGGING['loggers']['awx']['handlers'] = ['console', 'external_logger']
|
||||
LOGGING['loggers']['awx.main.commands.run_callback_receiver']['handlers'] = ['console']
|
||||
LOGGING['loggers']['awx.main.commands.inventory_import']['handlers'] = ['console']
|
||||
LOGGING['loggers']['awx.main.tasks']['handlers'] = ['console']
|
||||
LOGGING['loggers']['awx.main.scheduler']['handlers'] = ['console']
|
||||
LOGGING['loggers']['awx.main.tasks']['handlers'] = ['console', 'external_logger']
|
||||
LOGGING['loggers']['awx.main.scheduler']['handlers'] = ['console', 'external_logger']
|
||||
LOGGING['loggers']['django_auth_ldap']['handlers'] = ['console']
|
||||
LOGGING['loggers']['social']['handlers'] = ['console']
|
||||
LOGGING['loggers']['system_tracking_migrations']['handlers'] = ['console']
|
||||
|
||||
@ -68,8 +68,6 @@ SERVICE_NAME_DICT = {
|
||||
"channels": "awx-channels-worker",
|
||||
"uwsgi": "awx-uwsgi",
|
||||
"daphne": "awx-daphne"}
|
||||
# Used for sending commands in automatic restart
|
||||
UWSGI_FIFO_LOCATION = '/var/lib/awx/awxfifo'
|
||||
|
||||
# Store a snapshot of default settings at this point before loading any
|
||||
# customizable config files.
|
||||
|
||||
6
awx/sso/tests/test_env.py
Normal file
6
awx/sso/tests/test_env.py
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
# Ensure that our autouse overwrites are working
|
||||
def test_cache(settings):
|
||||
assert settings.CACHES['default']['BACKEND'] == 'django.core.cache.backends.locmem.LocMemCache'
|
||||
assert settings.CACHES['default']['LOCATION'].startswith('unique-')
|
||||
@ -157,8 +157,8 @@
|
||||
</div>
|
||||
|
||||
<div class="response-info" aria-label="{% trans "response info" %}">
|
||||
<pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}{% for key, val in response_headers|items %}
|
||||
<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>{% endfor %}
|
||||
<pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}{% if response_headers %}{% for key, val in response_headers|items %}
|
||||
<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>{% endfor %}{% endif %}
|
||||
{# Original line below had the side effect of also escaping content: #}
|
||||
{# </span>{{ content|urlize_quoted_links }}</pre>{% endautoescape %} #}
|
||||
{# For AWX, disable automatic URL creation and move content outside of autoescape off block. #}
|
||||
|
||||
@ -54,6 +54,18 @@ module.exports = {
|
||||
'no-multiple-empty-lines': ['error', { max: 1 }],
|
||||
'object-curly-newline': 'off',
|
||||
'space-before-function-paren': ['error', 'always'],
|
||||
'no-trailing-spaces': ['error']
|
||||
}
|
||||
'no-trailing-spaces': ['error'],
|
||||
'prefer-destructuring': ['error', {
|
||||
'VariableDeclarator': {
|
||||
'array': false,
|
||||
'object': true
|
||||
},
|
||||
'AssignmentExpression': {
|
||||
'array': false,
|
||||
'object': true
|
||||
}
|
||||
}, {
|
||||
'enforceForRenamedProperties': false
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
@import 'credentials/_index';
|
||||
@import 'output/_index';
|
||||
@import 'users/tokens/_index';
|
||||
|
||||
@ -3,10 +3,8 @@ function AddApplicationsController (models, $state, strings) {
|
||||
|
||||
const { application, me, organization } = models;
|
||||
const omit = [
|
||||
'authorization_grant_type',
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'client_type',
|
||||
'created',
|
||||
'modified',
|
||||
'related',
|
||||
@ -44,19 +42,16 @@ function AddApplicationsController (models, $state, strings) {
|
||||
vm.form.organization._resource = 'organization';
|
||||
vm.form.organization._route = 'applications.add.organization';
|
||||
vm.form.organization._model = organization;
|
||||
vm.form.organization._placeholder = strings.get('SELECT AN ORGANIZATION');
|
||||
vm.form.organization._placeholder = strings.get('inputs.ORGANIZATION_PLACEHOLDER');
|
||||
|
||||
vm.form.name.required = true;
|
||||
vm.form.organization.required = true;
|
||||
vm.form.redirect_uris.required = true;
|
||||
|
||||
delete vm.form.name.help_text;
|
||||
|
||||
vm.form.save = data => {
|
||||
const hiddenData = {
|
||||
authorization_grant_type: 'implicit',
|
||||
user: me.get('id'),
|
||||
client_type: 'public'
|
||||
user: me.get('id')
|
||||
};
|
||||
|
||||
const payload = _.merge(data, hiddenData);
|
||||
|
||||
@ -14,7 +14,9 @@
|
||||
<at-input-text col="4" tab="2" state="vm.form.description"></at-input-text>
|
||||
<at-input-lookup col="4" tab="3" state="vm.form.organization"></at-input-lookup>
|
||||
<at-divider></at-divider>
|
||||
<at-input-text col="4" tab="4" state="vm.form.redirect_uris"></at-input-text>
|
||||
<at-input-select col="4" tab="4" state="vm.form.authorization_grant_type"></at-input-select>
|
||||
<at-input-text col="4" tab="5" state="vm.form.redirect_uris"></at-input-text>
|
||||
<at-input-select col="4" tab="6" state="vm.form.client_type"></at-input-select>
|
||||
|
||||
<at-action-group col="12" pos="right">
|
||||
<at-form-action type="cancel" to="applications"></at-form-action>
|
||||
|
||||
@ -16,6 +16,10 @@ function ApplicationsStrings (BaseString) {
|
||||
USERS: t.s('Tokens')
|
||||
};
|
||||
|
||||
ns.tooltips = {
|
||||
ADD: t.s('Create a new Application')
|
||||
};
|
||||
|
||||
ns.add = {
|
||||
PANEL_TITLE: t.s('NEW APPLICATION')
|
||||
};
|
||||
@ -25,6 +29,10 @@ function ApplicationsStrings (BaseString) {
|
||||
ROW_ITEM_LABEL_ORGANIZATION: t.s('ORG'),
|
||||
ROW_ITEM_LABEL_MODIFIED: t.s('LAST MODIFIED')
|
||||
};
|
||||
|
||||
ns.inputs = {
|
||||
ORGANIZATION_PLACEHOLDER: t.s('SELECT AN ORGANIZATION')
|
||||
};
|
||||
}
|
||||
|
||||
ApplicationsStrings.$inject = ['BaseStringService'];
|
||||
|
||||
@ -4,10 +4,8 @@ function EditApplicationsController (models, $state, strings, $scope) {
|
||||
const { me, application, organization } = models;
|
||||
|
||||
const omit = [
|
||||
'authorization_grant_type',
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'client_type',
|
||||
'created',
|
||||
'modified',
|
||||
'related',
|
||||
@ -54,45 +52,30 @@ function EditApplicationsController (models, $state, strings, $scope) {
|
||||
|
||||
vm.form.disabled = !isEditable;
|
||||
|
||||
vm.form.name.required = true;
|
||||
|
||||
const isOrgAdmin = _.some(me.get('related.admin_of_organizations.results'), (org) => org.id === organization.get('id'));
|
||||
const isSuperuser = me.get('is_superuser');
|
||||
const isCurrentAuthor = Boolean(application.get('summary_fields.created_by.id') === me.get('id'));
|
||||
|
||||
vm.form.organization = {
|
||||
type: 'field',
|
||||
label: 'Organization',
|
||||
id: 'organization'
|
||||
};
|
||||
vm.form.description = {
|
||||
type: 'String',
|
||||
label: 'Description',
|
||||
id: 'description'
|
||||
};
|
||||
|
||||
vm.form.organization._resource = 'organization';
|
||||
vm.form.organization._route = 'applications.edit.organization';
|
||||
vm.form.organization._model = organization;
|
||||
vm.form.organization._placeholder = strings.get('SELECT AN ORGANIZATION');
|
||||
|
||||
// TODO: org not returned via api endpoint, check on this
|
||||
vm.form.organization._value = application.get('organization');
|
||||
|
||||
vm.form.organization._disabled = true;
|
||||
|
||||
if (isSuperuser || isOrgAdmin || (application.get('organization') === null && isCurrentAuthor)) {
|
||||
vm.form.organization._disabled = false;
|
||||
}
|
||||
|
||||
vm.form.name.required = true;
|
||||
vm.form.organization._resource = 'organization';
|
||||
vm.form.organization._model = organization;
|
||||
vm.form.organization._route = 'applications.edit.organization';
|
||||
vm.form.organization._value = application.get('summary_fields.organization.id');
|
||||
vm.form.organization._displayValue = application.get('summary_fields.organization.name');
|
||||
vm.form.organization._placeholder = strings.get('inputs.ORGANIZATION_PLACEHOLDER');
|
||||
vm.form.organization.required = true;
|
||||
vm.form.redirect_uris.required = true;
|
||||
|
||||
delete vm.form.name.help_text;
|
||||
|
||||
vm.form.save = data => {
|
||||
const hiddenData = {
|
||||
authorization_grant_type: 'implicit',
|
||||
user: me.get('id'),
|
||||
client_type: 'public'
|
||||
user: me.get('id')
|
||||
};
|
||||
|
||||
const payload = _.merge(data, hiddenData);
|
||||
|
||||
@ -62,8 +62,7 @@ function ApplicationsRun ($stateExtender, strings) {
|
||||
},
|
||||
data: {
|
||||
activityStream: true,
|
||||
// TODO: double-check activity stream works
|
||||
activityStreamTarget: 'application'
|
||||
activityStreamTarget: 'o_auth2_application'
|
||||
},
|
||||
views: {
|
||||
'@': {
|
||||
@ -111,8 +110,7 @@ function ApplicationsRun ($stateExtender, strings) {
|
||||
},
|
||||
data: {
|
||||
activityStream: true,
|
||||
// TODO: double-check activity stream works
|
||||
activityStreamTarget: 'application'
|
||||
activityStreamTarget: 'o_auth2_application'
|
||||
},
|
||||
views: {
|
||||
'add@applications': {
|
||||
@ -134,7 +132,7 @@ function ApplicationsRun ($stateExtender, strings) {
|
||||
},
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'application',
|
||||
activityStreamTarget: 'o_auth2_application',
|
||||
activityStreamId: 'application_id'
|
||||
},
|
||||
views: {
|
||||
@ -264,8 +262,7 @@ function ApplicationsRun ($stateExtender, strings) {
|
||||
},
|
||||
data: {
|
||||
activityStream: true,
|
||||
// TODO: double-check activity stream works
|
||||
activityStreamTarget: 'application'
|
||||
activityStreamTarget: 'o_auth2_application'
|
||||
},
|
||||
views: {
|
||||
'userList@applications.edit': {
|
||||
|
||||
@ -38,6 +38,10 @@ function ListApplicationsController (
|
||||
vm.applicationsCount = dataset.count;
|
||||
});
|
||||
|
||||
vm.tooltips = {
|
||||
add: strings.get('tooltips.ADD')
|
||||
};
|
||||
|
||||
vm.getModified = app => {
|
||||
const modified = _.get(app, 'modified');
|
||||
|
||||
@ -74,7 +78,7 @@ function ListApplicationsController (
|
||||
}
|
||||
|
||||
if (parseInt($state.params.application_id, 10) === app.id) {
|
||||
$state.go('^', reloadListStateParams, { reload: true });
|
||||
$state.go('applications', reloadListStateParams, { reload: true });
|
||||
} else {
|
||||
$state.go('.', reloadListStateParams, { reload: true });
|
||||
}
|
||||
|
||||
@ -23,6 +23,9 @@
|
||||
type="button"
|
||||
ui-sref="applications.add"
|
||||
class="at-Button--add"
|
||||
id="button-add"
|
||||
aw-tool-tip="{{vm.tooltips.add}}"
|
||||
data-placement="top"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
</button>
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
.at-CredentialsPermissions {
|
||||
margin-top: 50px;
|
||||
}
|
||||
@ -69,8 +69,8 @@ function LegacyCredentialsService () {
|
||||
ngClick: '$state.go(\'.add\')',
|
||||
label: 'Add',
|
||||
awToolTip: N_('Add a permission'),
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
buttonContent: `+ ${N_('ADD')}`,
|
||||
actionClass: 'at-Button--add',
|
||||
actionId: 'button-add',
|
||||
ngShow: '(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
}
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user