Merge branch 'notifications_work' into devel

* notifications_work: (23 commits)
  Updates to notification unit tests after @wwitzel3's feedback
  Fix some notifications issues and write some tests
  Add notification system documentation
  Clean up flake8 related issues
  Fixing up some unicode issues
  Implement tower ui view url on models
  Sanity check and force proper types in admin check
  Proper type for in check
  Adding migration and base notification type
  Add a periodic administrative notification
  Refactor message generator
  Support notification password field encryption
  Notification configuration type checking
  Refactor NotificationTemplate to Notifier
  Implement irc notification backend
  Add webhook notification backend
  Pagerduty and Hipchat backends plus some cleanup
  Notification serializers, views, and tasks
  Implement notification serializer and validations
  Notification endpoints and url expositions
  ...
This commit is contained in:
Matthew Jones
2016-02-29 16:50:33 -05:00
35 changed files with 1575 additions and 18 deletions

View File

@@ -13,7 +13,7 @@ from rest_framework import serializers
from rest_framework.request import clone_request
# Ansible Tower
from awx.main.models import InventorySource
from awx.main.models import InventorySource, Notifier
class Metadata(metadata.SimpleMetadata):
@@ -76,6 +76,12 @@ class Metadata(metadata.SimpleMetadata):
get_group_by_choices = getattr(InventorySource, 'get_%s_group_by_choices' % cp)
field_info['%s_group_by_choices' % cp] = get_group_by_choices()
# Special handling of notification configuration where the required properties
# are conditional on the type selected.
if field.field_name == 'notification_configuration':
for (notification_type_name, notification_tr_name, notification_type_class) in Notifier.NOTIFICATION_TYPES:
field_info[notification_type_name] = notification_type_class.init_parameters
# Update type of fields returned...
if field.field_name == 'type':
field_info['type'] = 'multiple choice'

View File

@@ -481,6 +481,8 @@ class BaseSerializer(serializers.ModelSerializer):
class EmptySerializer(serializers.Serializer):
pass
class EmptySerializer(serializers.Serializer):
pass
class BaseFactSerializer(DocumentSerializer):
@@ -794,7 +796,11 @@ class OrganizationSerializer(BaseSerializer):
users = reverse('api:organization_users_list', args=(obj.pk,)),
admins = reverse('api:organization_admins_list', args=(obj.pk,)),
teams = reverse('api:organization_teams_list', args=(obj.pk,)),
activity_stream = reverse('api:organization_activity_stream_list', args=(obj.pk,))
activity_stream = reverse('api:organization_activity_stream_list', args=(obj.pk,)),
notifiers = reverse('api:organization_notifiers_list', args=(obj.pk,)),
notifiers_any = reverse('api:organization_notifiers_any_list', args=(obj.pk,)),
notifiers_success = reverse('api:organization_notifiers_success_list', args=(obj.pk,)),
notifiers_error = reverse('api:organization_notifiers_error_list', args=(obj.pk,)),
))
return res
@@ -864,6 +870,9 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
project_updates = reverse('api:project_updates_list', args=(obj.pk,)),
schedules = reverse('api:project_schedules_list', args=(obj.pk,)),
activity_stream = reverse('api:project_activity_stream_list', args=(obj.pk,)),
notifiers_any = reverse('api:project_notifiers_any_list', args=(obj.pk,)),
notifiers_success = reverse('api:project_notifiers_success_list', args=(obj.pk,)),
notifiers_error = reverse('api:project_notifiers_error_list', args=(obj.pk,)),
))
# Backwards compatibility.
if obj.current_update:
@@ -909,6 +918,7 @@ class ProjectUpdateSerializer(UnifiedJobSerializer, ProjectOptionsSerializer):
res.update(dict(
project = reverse('api:project_detail', args=(obj.project.pk,)),
cancel = reverse('api:project_update_cancel', args=(obj.pk,)),
notifications = reverse('api:project_update_notifications_list', args=(obj.pk,)),
))
return res
@@ -1316,6 +1326,9 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
activity_stream = reverse('api:inventory_activity_stream_list', args=(obj.pk,)),
hosts = reverse('api:inventory_source_hosts_list', args=(obj.pk,)),
groups = reverse('api:inventory_source_groups_list', args=(obj.pk,)),
notifiers_any = reverse('api:inventory_source_notifiers_any_list', args=(obj.pk,)),
notifiers_success = reverse('api:inventory_source_notifiers_success_list', args=(obj.pk,)),
notifiers_error = reverse('api:inventory_source_notifiers_error_list', args=(obj.pk,)),
))
if obj.inventory and obj.inventory.active:
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
@@ -1360,6 +1373,7 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri
res.update(dict(
inventory_source = reverse('api:inventory_source_detail', args=(obj.inventory_source.pk,)),
cancel = reverse('api:inventory_update_cancel', args=(obj.pk,)),
notifications = reverse('api:inventory_update_notifications_list', args=(obj.pk,)),
))
return res
@@ -1575,6 +1589,9 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
schedules = reverse('api:job_template_schedules_list', args=(obj.pk,)),
activity_stream = reverse('api:job_template_activity_stream_list', args=(obj.pk,)),
launch = reverse('api:job_template_launch', args=(obj.pk,)),
notifiers_any = reverse('api:job_template_notifiers_any_list', args=(obj.pk,)),
notifiers_success = reverse('api:job_template_notifiers_success_list', args=(obj.pk,)),
notifiers_error = reverse('api:job_template_notifiers_error_list', args=(obj.pk,)),
))
if obj.host_config_key:
res['callback'] = reverse('api:job_template_callback', args=(obj.pk,))
@@ -1629,6 +1646,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
job_tasks = reverse('api:job_job_tasks_list', args=(obj.pk,)),
job_host_summaries = reverse('api:job_job_host_summaries_list', args=(obj.pk,)),
activity_stream = reverse('api:job_activity_stream_list', args=(obj.pk,)),
notifications = reverse('api:job_notifications_list', args=(obj.pk,)),
))
if obj.job_template and obj.job_template.active:
res['job_template'] = reverse('api:job_template_detail',
@@ -2044,6 +2062,79 @@ class JobLaunchSerializer(BaseSerializer):
attrs = super(JobLaunchSerializer, self).validate(attrs)
return attrs
class NotifierSerializer(BaseSerializer):
class Meta:
model = Notifier
fields = ('*', 'organization', 'notification_type', 'notification_configuration')
type_map = {"string": (str, unicode),
"int": (int,),
"bool": (bool,),
"list": (list,),
"password": (str, unicode),
"object": (dict,)}
def to_representation(self, obj):
ret = super(NotifierSerializer, self).to_representation(obj)
for field in obj.notification_class.init_parameters:
if field in ret['notification_configuration'] and \
force_text(ret['notification_configuration'][field]).startswith('$encrypted$'):
ret['notification_configuration'][field] = '$encrypted$'
return ret
def get_related(self, obj):
res = super(NotifierSerializer, self).get_related(obj)
res.update(dict(
test = reverse('api:notifier_test', args=(obj.pk,)),
notifications = reverse('api:notifier_notification_list', args=(obj.pk,)),
))
if obj.organization and obj.organization.active:
res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,))
return res
def validate(self, attrs):
notification_class = Notifier.CLASS_FOR_NOTIFICATION_TYPE[attrs['notification_type']]
missing_fields = []
incorrect_type_fields = []
if 'notification_configuration' not in attrs:
return attrs
for field in notification_class.init_parameters:
if field not in attrs['notification_configuration']:
missing_fields.append(field)
continue
field_val = attrs['notification_configuration'][field]
field_type = notification_class.init_parameters[field]['type']
expected_types = self.type_map[field_type]
if not type(field_val) in expected_types:
incorrect_type_fields.append((field, field_type))
continue
if field_type == "password" and field_val.startswith('$encrypted$'):
missing_fields.append(field)
error_list = []
if missing_fields:
error_list.append("Missing required fields for Notification Configuration: {}".format(missing_fields))
if incorrect_type_fields:
for type_field_error in incorrect_type_fields:
error_list.append("Configuration field '{}' incorrect type, expected {}".format(type_field_error[0],
type_field_error[1]))
if error_list:
raise serializers.ValidationError(error_list)
return attrs
class NotificationSerializer(BaseSerializer):
class Meta:
model = Notification
fields = ('*', '-name', '-description', 'notifier', 'error', 'status', 'notifications_sent',
'notification_type', 'recipients', 'subject')
def get_related(self, obj):
res = super(NotificationSerializer, self).get_related(obj)
res.update(dict(
notifier = reverse('api:notifier_detail', args=(obj.notifier.pk,)),
))
return res
class ScheduleSerializer(BaseSerializer):

View File

@@ -20,6 +20,10 @@ organization_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/projects/$', 'organization_projects_list'),
url(r'^(?P<pk>[0-9]+)/teams/$', 'organization_teams_list'),
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'organization_activity_stream_list'),
url(r'^(?P<pk>[0-9]+)/notifiers/$', 'organization_notifiers_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_any/$', 'organization_notifiers_any_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_error/$', 'organization_notifiers_error_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'organization_notifiers_success_list'),
)
user_urls = patterns('awx.api.views',
@@ -44,12 +48,16 @@ project_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/project_updates/$', 'project_updates_list'),
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'project_activity_stream_list'),
url(r'^(?P<pk>[0-9]+)/schedules/$', 'project_schedules_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_any/$', 'project_notifiers_any_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_error/$', 'project_notifiers_error_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'project_notifiers_success_list'),
)
project_update_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/$', 'project_update_detail'),
url(r'^(?P<pk>[0-9]+)/cancel/$', 'project_update_cancel'),
url(r'^(?P<pk>[0-9]+)/stdout/$', 'project_update_stdout'),
url(r'^(?P<pk>[0-9]+)/notifications/$', 'project_update_notifications_list'),
)
team_urls = patterns('awx.api.views',
@@ -121,12 +129,16 @@ inventory_source_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/schedules/$', 'inventory_source_schedules_list'),
url(r'^(?P<pk>[0-9]+)/groups/$', 'inventory_source_groups_list'),
url(r'^(?P<pk>[0-9]+)/hosts/$', 'inventory_source_hosts_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_any/$', 'inventory_source_notifiers_any_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_error/$', 'inventory_source_notifiers_error_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'inventory_source_notifiers_success_list'),
)
inventory_update_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/$', 'inventory_update_detail'),
url(r'^(?P<pk>[0-9]+)/cancel/$', 'inventory_update_cancel'),
url(r'^(?P<pk>[0-9]+)/stdout/$', 'inventory_update_stdout'),
url(r'^(?P<pk>[0-9]+)/notifications/$', 'inventory_update_notifications_list'),
)
inventory_script_urls = patterns('awx.api.views',
@@ -154,6 +166,9 @@ job_template_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/schedules/$', 'job_template_schedules_list'),
url(r'^(?P<pk>[0-9]+)/survey_spec/$', 'job_template_survey_spec'),
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'job_template_activity_stream_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_any/$', 'job_template_notifiers_any_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_error/$', 'job_template_notifiers_error_list'),
url(r'^(?P<pk>[0-9]+)/notifiers_success/$', 'job_template_notifiers_success_list'),
)
job_urls = patterns('awx.api.views',
@@ -168,6 +183,7 @@ job_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/job_tasks/$', 'job_job_tasks_list'),
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'job_activity_stream_list'),
url(r'^(?P<pk>[0-9]+)/stdout/$', 'job_stdout'),
url(r'^(?P<pk>[0-9]+)/notifications/$', 'job_notifications_list'),
)
job_host_summary_urls = patterns('awx.api.views',
@@ -210,6 +226,18 @@ system_job_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/cancel/$', 'system_job_cancel'),
)
notifier_urls = patterns('awx.api.views',
url(r'^$', 'notifier_list'),
url(r'^(?P<pk>[0-9]+)/$', 'notifier_detail'),
url(r'^(?P<pk>[0-9]+)/test/$', 'notifier_test'),
url(r'^(?P<pk>[0-9]+)/notifications/$', 'notifier_notification_list'),
)
notification_urls = patterns('awx.api.views',
url(r'^$', 'notification_list'),
url(r'^(?P<pk>[0-9]+)/$', 'notification_detail'),
)
schedule_urls = patterns('awx.api.views',
url(r'^$', 'schedule_list'),
url(r'^(?P<pk>[0-9]+)/$', 'schedule_detail'),
@@ -258,6 +286,8 @@ v1_urls = patterns('awx.api.views',
url(r'^ad_hoc_command_events/', include(ad_hoc_command_event_urls)),
url(r'^system_job_templates/', include(system_job_template_urls)),
url(r'^system_jobs/', include(system_job_urls)),
url(r'^notifiers/', include(notifier_urls)),
url(r'^notifications/', include(notification_urls)),
url(r'^unified_job_templates/$', 'unified_job_template_list'),
url(r'^unified_jobs/$', 'unified_job_list'),
url(r'^activity_stream/', include(activity_stream_urls)),

View File

@@ -56,7 +56,7 @@ from social.backends.utils import load_backends
# AWX
from awx.main.task_engine import TaskSerializer, TASK_FILE, TEMPORARY_TASK_FILE
from awx.main.tasks import mongodb_control
from awx.main.tasks import mongodb_control, send_notifications
from awx.main.access import get_user_queryset
from awx.main.ha import is_ha_environment
from awx.api.authentication import TaskAuthentication, TokenGetAuthentication
@@ -135,6 +135,8 @@ class ApiV1RootView(APIView):
data['system_job_templates'] = reverse('api:system_job_template_list')
data['system_jobs'] = reverse('api:system_job_list')
data['schedules'] = reverse('api:schedule_list')
data['notifiers'] = reverse('api:notifier_list')
data['notifications'] = reverse('api:notification_list')
data['unified_job_templates'] = reverse('api:unified_job_template_list')
data['unified_jobs'] = reverse('api:unified_job_list')
data['activity_stream'] = reverse('api:activity_stream_list')
@@ -266,6 +268,7 @@ class ApiV1ConfigView(APIView):
# If the license is valid, write it to disk.
if license_data['valid_key']:
tower_settings.LICENSE = data_actual
tower_settings.TOWER_URL_BASE = "{}://{}".format(request.scheme, request.get_host())
# Spawn a task to ensure that MongoDB is started (or stopped)
# as appropriate, based on whether the license uses it.
@@ -696,6 +699,35 @@ class OrganizationActivityStreamList(SubListAPIView):
# Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs)
class OrganizationNotifiersList(SubListCreateAttachDetachAPIView):
model = Notifier
serializer_class = NotifierSerializer
parent_model = Organization
relationship = 'notifiers'
parent_key = 'organization'
class OrganizationNotifiersAnyList(SubListCreateAttachDetachAPIView):
model = Notifier
serializer_class = NotifierSerializer
parent_model = Organization
relationship = 'notifiers_any'
class OrganizationNotifiersErrorList(SubListCreateAttachDetachAPIView):
model = Notifier
serializer_class = NotifierSerializer
parent_model = Organization
relationship = 'notifiers_error'
class OrganizationNotifiersSuccessList(SubListCreateAttachDetachAPIView):
model = Notifier
serializer_class = NotifierSerializer
parent_model = Organization
relationship = 'notifiers_success'
class TeamList(ListCreateAPIView):
model = Team
@@ -861,6 +893,26 @@ class ProjectActivityStreamList(SubListAPIView):
return qs.filter(project=parent)
return qs.filter(Q(project=parent) | Q(credential__in=parent.credential))
class ProjectNotifiersAnyList(SubListCreateAttachDetachAPIView):
model = Notifier
serializer_class = NotifierSerializer
parent_model = Project
relationship = 'notifiers_any'
class ProjectNotifiersErrorList(SubListCreateAttachDetachAPIView):
model = Notifier
serializer_class = NotifierSerializer
parent_model = Project
relationship = 'notifiers_error'
class ProjectNotifiersSuccessList(SubListCreateAttachDetachAPIView):
model = Notifier
serializer_class = NotifierSerializer
parent_model = Project
relationship = 'notifiers_success'
class ProjectUpdatesList(SubListAPIView):
@@ -911,6 +963,12 @@ class ProjectUpdateCancel(RetrieveAPIView):
else:
return self.http_method_not_allowed(request, *args, **kwargs)
class ProjectUpdateNotificationsList(SubListAPIView):
model = Notification
serializer_class = NotificationSerializer
parent_model = Project
relationship = 'notifications'
class UserList(ListCreateAPIView):
@@ -1783,6 +1841,27 @@ class InventorySourceActivityStreamList(SubListAPIView):
# Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs)
class InventorySourceNotifiersAnyList(SubListCreateAttachDetachAPIView):
model = Notifier
serializer_class = NotifierSerializer
parent_model = InventorySource
relationship = 'notifiers_any'
class InventorySourceNotifiersErrorList(SubListCreateAttachDetachAPIView):
model = Notifier
serializer_class = NotifierSerializer
parent_model = InventorySource
relationship = 'notifiers_error'
class InventorySourceNotifiersSuccessList(SubListCreateAttachDetachAPIView):
model = Notifier
serializer_class = NotifierSerializer
parent_model = InventorySource
relationship = 'notifiers_success'
class InventorySourceHostsList(SubListAPIView):
model = Host
@@ -1847,6 +1926,13 @@ class InventoryUpdateCancel(RetrieveAPIView):
else:
return self.http_method_not_allowed(request, *args, **kwargs)
class InventoryUpdateNotificationsList(SubListAPIView):
model = Notification
serializer_class = NotificationSerializer
parent_model = InventoryUpdate
relationship = 'notifications'
class JobTemplateList(ListCreateAPIView):
model = JobTemplate
@@ -2016,6 +2102,27 @@ class JobTemplateActivityStreamList(SubListAPIView):
# Okay, let it through.
return super(type(self), self).get(request, *args, **kwargs)
class JobTemplateNotifiersAnyList(SubListCreateAttachDetachAPIView):
model = Notifier
serializer_class = NotifierSerializer
parent_model = JobTemplate
relationship = 'notifiers_any'
class JobTemplateNotifiersErrorList(SubListCreateAttachDetachAPIView):
model = Notifier
serializer_class = NotifierSerializer
parent_model = JobTemplate
relationship = 'notifiers_error'
class JobTemplateNotifiersSuccessList(SubListCreateAttachDetachAPIView):
model = Notifier
serializer_class = NotifierSerializer
parent_model = JobTemplate
relationship = 'notifiers_success'
class JobTemplateCallback(GenericAPIView):
model = JobTemplate
@@ -2349,6 +2456,13 @@ class JobRelaunch(RetrieveAPIView, GenericAPIView):
headers = {'Location': new_job.get_absolute_url()}
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
class JobNotificationsList(SubListAPIView):
model = Notification
serializer_class = NotificationSerializer
parent_model = Job
relationship = 'notifications'
class BaseJobHostSummariesList(SubListAPIView):
model = JobHostSummary
@@ -3002,6 +3116,58 @@ class AdHocCommandStdout(UnifiedJobStdout):
model = AdHocCommand
new_in_220 = True
class NotifierList(ListCreateAPIView):
model = Notifier
serializer_class = NotifierSerializer
new_in_300 = True
class NotifierDetail(RetrieveUpdateDestroyAPIView):
model = Notifier
serializer_class = NotifierSerializer
new_in_300 = True
class NotifierTest(GenericAPIView):
view_name = 'Notifier Test'
model = Notifier
serializer_class = EmptySerializer
new_in_300 = True
def post(self, request, *args, **kwargs):
obj = self.get_object()
notification = obj.generate_notification("Tower Notification Test {} {}".format(obj.id, tower_settings.TOWER_URL_BASE),
{"body": "Ansible Tower Test Notification {} {}".format(obj.id, tower_settings.TOWER_URL_BASE)})
if not notification:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
else:
send_notifications.delay([notification.id])
headers = {'Location': notification.get_absolute_url()}
return Response({"notification": notification.id},
headers=headers,
status=status.HTTP_202_ACCEPTED)
class NotifierNotificationList(SubListAPIView):
model = Notification
serializer_class = NotificationSerializer
parent_model = Notifier
relationship = 'notifications'
parent_key = 'notifier'
class NotificationList(ListAPIView):
model = Notification
serializer_class = NotificationSerializer
new_in_300 = True
class NotificationDetail(RetrieveAPIView):
model = Notification
serializer_class = NotificationSerializer
new_in_300 = True
class ActivityStreamList(SimpleListAPIView):
model = ActivityStream