mirror of
https://github.com/ansible/awx.git
synced 2026-02-26 23:46:05 -03:30
Notification serializers, views, and tasks
* Implement concrete Notification model for notification runs * Implement NotificationTemplate and Notification serializers and views * Implement ancillary views * Implement NotificationTemplate trigger m2m fields on all job templates via a fields mixin * Link NotificationTemplates with an org * Link notifications with the activity stream * Implement Notification celery tasks * Extend Backend field parameters to identify sender and receiver as parameters needed by the message and not the backend itself * Updates to backends to better fit the django email backend model as it relates to Messages * Implement success job chain task + notifications * Implement notifications in error job chain task
This commit is contained in:
@@ -457,6 +457,8 @@ class BaseSerializer(serializers.ModelSerializer):
|
|||||||
ret.pop(parent_key, None)
|
ret.pop(parent_key, None)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
class EmptySerializer(serializers.Serializer):
|
||||||
|
pass
|
||||||
|
|
||||||
class BaseFactSerializer(DocumentSerializer):
|
class BaseFactSerializer(DocumentSerializer):
|
||||||
|
|
||||||
@@ -765,7 +767,11 @@ class OrganizationSerializer(BaseSerializer):
|
|||||||
users = reverse('api:organization_users_list', args=(obj.pk,)),
|
users = reverse('api:organization_users_list', args=(obj.pk,)),
|
||||||
admins = reverse('api:organization_admins_list', args=(obj.pk,)),
|
admins = reverse('api:organization_admins_list', args=(obj.pk,)),
|
||||||
teams = reverse('api:organization_teams_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_notifications_any_list', args=(obj.pk,)),
|
||||||
|
notifiers_success = reverse('api:organization_notifications_success_list', args=(obj.pk,)),
|
||||||
|
notifiers_error = reverse('api:organization_notifications_error_list', args=(obj.pk,)),
|
||||||
))
|
))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -845,6 +851,9 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
|
|||||||
project_updates = reverse('api:project_updates_list', args=(obj.pk,)),
|
project_updates = reverse('api:project_updates_list', args=(obj.pk,)),
|
||||||
schedules = reverse('api:project_schedules_list', args=(obj.pk,)),
|
schedules = reverse('api:project_schedules_list', args=(obj.pk,)),
|
||||||
activity_stream = reverse('api:project_activity_stream_list', args=(obj.pk,)),
|
activity_stream = reverse('api:project_activity_stream_list', args=(obj.pk,)),
|
||||||
|
notifiers_any = reverse('api:project_notifications_any_list', args=(obj.pk,)),
|
||||||
|
notifiers_success = reverse('api:project_notifications_success_list', args=(obj.pk,)),
|
||||||
|
notifiers_error = reverse('api:project_notifications_error_list', args=(obj.pk,)),
|
||||||
))
|
))
|
||||||
# Backwards compatibility.
|
# Backwards compatibility.
|
||||||
if obj.current_update:
|
if obj.current_update:
|
||||||
@@ -888,6 +897,7 @@ class ProjectUpdateSerializer(UnifiedJobSerializer, ProjectOptionsSerializer):
|
|||||||
res.update(dict(
|
res.update(dict(
|
||||||
project = reverse('api:project_detail', args=(obj.project.pk,)),
|
project = reverse('api:project_detail', args=(obj.project.pk,)),
|
||||||
cancel = reverse('api:project_update_cancel', args=(obj.pk,)),
|
cancel = reverse('api:project_update_cancel', args=(obj.pk,)),
|
||||||
|
notifications = reverse('api:project_update_notifications_list', args=(obj.pk,)),
|
||||||
))
|
))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -1288,6 +1298,9 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
|||||||
activity_stream = reverse('api:inventory_activity_stream_list', args=(obj.pk,)),
|
activity_stream = reverse('api:inventory_activity_stream_list', args=(obj.pk,)),
|
||||||
hosts = reverse('api:inventory_source_hosts_list', args=(obj.pk,)),
|
hosts = reverse('api:inventory_source_hosts_list', args=(obj.pk,)),
|
||||||
groups = reverse('api:inventory_source_groups_list', args=(obj.pk,)),
|
groups = reverse('api:inventory_source_groups_list', args=(obj.pk,)),
|
||||||
|
notifiers_any = reverse('api:inventory_source_notifications_any_list', args=(obj.pk,)),
|
||||||
|
notifiers_success = reverse('api:inventory_source_notifications_success_list', args=(obj.pk,)),
|
||||||
|
notifiers_error = reverse('api:inventory_source_notifications_error_list', args=(obj.pk,)),
|
||||||
))
|
))
|
||||||
if obj.inventory and obj.inventory.active:
|
if obj.inventory and obj.inventory.active:
|
||||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||||
@@ -1332,6 +1345,7 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri
|
|||||||
res.update(dict(
|
res.update(dict(
|
||||||
inventory_source = reverse('api:inventory_source_detail', args=(obj.inventory_source.pk,)),
|
inventory_source = reverse('api:inventory_source_detail', args=(obj.inventory_source.pk,)),
|
||||||
cancel = reverse('api:inventory_update_cancel', args=(obj.pk,)),
|
cancel = reverse('api:inventory_update_cancel', args=(obj.pk,)),
|
||||||
|
notifications = reverse('api:inventory_update_notifications_list', args=(obj.pk,)),
|
||||||
))
|
))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -1550,6 +1564,9 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
|||||||
schedules = reverse('api:job_template_schedules_list', args=(obj.pk,)),
|
schedules = reverse('api:job_template_schedules_list', args=(obj.pk,)),
|
||||||
activity_stream = reverse('api:job_template_activity_stream_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,)),
|
launch = reverse('api:job_template_launch', args=(obj.pk,)),
|
||||||
|
notifiers_any = reverse('api:job_template_notifications_any_list', args=(obj.pk,)),
|
||||||
|
notifiers_success = reverse('api:job_template_notifications_success_list', args=(obj.pk,)),
|
||||||
|
notifiers_error = reverse('api:job_template_notifications_error_list', args=(obj.pk,)),
|
||||||
))
|
))
|
||||||
if obj.host_config_key:
|
if obj.host_config_key:
|
||||||
res['callback'] = reverse('api:job_template_callback', args=(obj.pk,))
|
res['callback'] = reverse('api:job_template_callback', args=(obj.pk,))
|
||||||
@@ -1604,6 +1621,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
|||||||
job_tasks = reverse('api:job_job_tasks_list', args=(obj.pk,)),
|
job_tasks = reverse('api:job_job_tasks_list', args=(obj.pk,)),
|
||||||
job_host_summaries = reverse('api:job_job_host_summaries_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,)),
|
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:
|
if obj.job_template and obj.job_template.active:
|
||||||
res['job_template'] = reverse('api:job_template_detail',
|
res['job_template'] = reverse('api:job_template_detail',
|
||||||
@@ -2039,7 +2057,15 @@ class NotificationTemplateSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = NotificationTemplate
|
model = NotificationTemplate
|
||||||
fields = ('*', 'notification_type', 'notification_configuration')
|
fields = ('*', 'organization', 'notification_type', 'notification_configuration')
|
||||||
|
|
||||||
|
def get_related(self, obj):
|
||||||
|
res = super(NotificationTemplateSerializer, self).get_related(obj)
|
||||||
|
res.update(dict(
|
||||||
|
test = reverse('api:notification_template_test', args=(obj.pk,)),
|
||||||
|
notifications = reverse('api:notification_template_notification_list', args=(obj.pk,)),
|
||||||
|
))
|
||||||
|
return res
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
notification_class = NotificationTemplate.CLASS_FOR_NOTIFICATION_TYPE[attrs['notification_type']]
|
notification_class = NotificationTemplate.CLASS_FOR_NOTIFICATION_TYPE[attrs['notification_type']]
|
||||||
@@ -2047,10 +2073,25 @@ class NotificationTemplateSerializer(BaseSerializer):
|
|||||||
for field in notification_class.init_parameters:
|
for field in notification_class.init_parameters:
|
||||||
if field not in attrs['notification_configuration']:
|
if field not in attrs['notification_configuration']:
|
||||||
missing_fields.append(field)
|
missing_fields.append(field)
|
||||||
|
# TODO: Type checks
|
||||||
if missing_fields:
|
if missing_fields:
|
||||||
raise serializers.ValidationError("Missing required fields for Notification Configuration: {}".format(missing_fields))
|
raise serializers.ValidationError("Missing required fields for Notification Configuration: {}".format(missing_fields))
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
class NotificationSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Notification
|
||||||
|
fields = ('*', '-name', '-description', 'notifier', 'error', 'status', 'notifications_sent',
|
||||||
|
'notification_type', 'recipients', 'subject', 'body')
|
||||||
|
|
||||||
|
def get_related(self, obj):
|
||||||
|
res = super(NotificationSerializer, self).get_related(obj)
|
||||||
|
res.update(dict(
|
||||||
|
notification_template = reverse('api:notification_template_detail', args=(obj.notifier.pk,)),
|
||||||
|
))
|
||||||
|
return res
|
||||||
|
|
||||||
class ScheduleSerializer(BaseSerializer):
|
class ScheduleSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -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]+)/projects/$', 'organization_projects_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/teams/$', 'organization_teams_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]+)/activity_stream/$', 'organization_activity_stream_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifiers/$', 'organization_notifiers_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifications_any/$', 'organization_notifications_any_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifications_error/$', 'organization_notifications_error_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifications_success/$', 'organization_notifications_success_list'),
|
||||||
)
|
)
|
||||||
|
|
||||||
user_urls = patterns('awx.api.views',
|
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]+)/project_updates/$', 'project_updates_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'project_activity_stream_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]+)/schedules/$', 'project_schedules_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifications_any/$', 'project_notifications_any_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifications_error/$', 'project_notifications_error_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifications_success/$', 'project_notifications_success_list'),
|
||||||
)
|
)
|
||||||
|
|
||||||
project_update_urls = patterns('awx.api.views',
|
project_update_urls = patterns('awx.api.views',
|
||||||
url(r'^(?P<pk>[0-9]+)/$', 'project_update_detail'),
|
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]+)/cancel/$', 'project_update_cancel'),
|
||||||
url(r'^(?P<pk>[0-9]+)/stdout/$', 'project_update_stdout'),
|
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',
|
team_urls = patterns('awx.api.views',
|
||||||
@@ -120,12 +128,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]+)/schedules/$', 'inventory_source_schedules_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/groups/$', 'inventory_source_groups_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]+)/hosts/$', 'inventory_source_hosts_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifications_any/$', 'inventory_source_notifications_any_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifications_error/$', 'inventory_source_notifications_error_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifications_success/$', 'inventory_source_notifications_success_list'),
|
||||||
)
|
)
|
||||||
|
|
||||||
inventory_update_urls = patterns('awx.api.views',
|
inventory_update_urls = patterns('awx.api.views',
|
||||||
url(r'^(?P<pk>[0-9]+)/$', 'inventory_update_detail'),
|
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]+)/cancel/$', 'inventory_update_cancel'),
|
||||||
url(r'^(?P<pk>[0-9]+)/stdout/$', 'inventory_update_stdout'),
|
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',
|
inventory_script_urls = patterns('awx.api.views',
|
||||||
@@ -153,6 +165,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]+)/schedules/$', 'job_template_schedules_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/survey_spec/$', 'job_template_survey_spec'),
|
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]+)/activity_stream/$', 'job_template_activity_stream_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifications_any/$', 'job_template_notifications_any_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifications_error/$', 'job_template_notifications_error_list'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifications_success/$', 'job_template_notifications_success_list'),
|
||||||
)
|
)
|
||||||
|
|
||||||
job_urls = patterns('awx.api.views',
|
job_urls = patterns('awx.api.views',
|
||||||
@@ -167,6 +182,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]+)/job_tasks/$', 'job_job_tasks_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'job_activity_stream_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]+)/stdout/$', 'job_stdout'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifications/$', 'job_notifications_list'),
|
||||||
)
|
)
|
||||||
|
|
||||||
job_host_summary_urls = patterns('awx.api.views',
|
job_host_summary_urls = patterns('awx.api.views',
|
||||||
@@ -212,6 +228,13 @@ system_job_urls = patterns('awx.api.views',
|
|||||||
notification_template_urls = patterns('awx.api.views',
|
notification_template_urls = patterns('awx.api.views',
|
||||||
url(r'^$', 'notification_template_list'),
|
url(r'^$', 'notification_template_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/$', 'notification_template_detail'),
|
url(r'^(?P<pk>[0-9]+)/$', 'notification_template_detail'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/test/$', 'notification_template_test'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/notifications/$', 'notification_template_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',
|
schedule_urls = patterns('awx.api.views',
|
||||||
@@ -263,6 +286,7 @@ v1_urls = patterns('awx.api.views',
|
|||||||
url(r'^system_job_templates/', include(system_job_template_urls)),
|
url(r'^system_job_templates/', include(system_job_template_urls)),
|
||||||
url(r'^system_jobs/', include(system_job_urls)),
|
url(r'^system_jobs/', include(system_job_urls)),
|
||||||
url(r'^notification_templates/', include(notification_template_urls)),
|
url(r'^notification_templates/', include(notification_template_urls)),
|
||||||
|
url(r'^notifications/', include(notification_urls)),
|
||||||
url(r'^unified_job_templates/$', 'unified_job_template_list'),
|
url(r'^unified_job_templates/$', 'unified_job_template_list'),
|
||||||
url(r'^unified_jobs/$', 'unified_job_list'),
|
url(r'^unified_jobs/$', 'unified_job_list'),
|
||||||
url(r'^activity_stream/', include(activity_stream_urls)),
|
url(r'^activity_stream/', include(activity_stream_urls)),
|
||||||
|
|||||||
157
awx/api/views.py
157
awx/api/views.py
@@ -56,7 +56,7 @@ from social.backends.utils import load_backends
|
|||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.task_engine import TaskSerializer, TASK_FILE, TEMPORARY_TASK_FILE
|
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.access import get_user_queryset
|
||||||
from awx.main.ha import is_ha_environment
|
from awx.main.ha import is_ha_environment
|
||||||
from awx.api.authentication import TaskAuthentication, TokenGetAuthentication
|
from awx.api.authentication import TaskAuthentication, TokenGetAuthentication
|
||||||
@@ -136,6 +136,7 @@ class ApiV1RootView(APIView):
|
|||||||
data['system_jobs'] = reverse('api:system_job_list')
|
data['system_jobs'] = reverse('api:system_job_list')
|
||||||
data['schedules'] = reverse('api:schedule_list')
|
data['schedules'] = reverse('api:schedule_list')
|
||||||
data['notification_templates'] = reverse('api:notification_template_list')
|
data['notification_templates'] = reverse('api:notification_template_list')
|
||||||
|
data['notifications'] = reverse('api:notification_list')
|
||||||
data['unified_job_templates'] = reverse('api:unified_job_template_list')
|
data['unified_job_templates'] = reverse('api:unified_job_template_list')
|
||||||
data['unified_jobs'] = reverse('api:unified_job_list')
|
data['unified_jobs'] = reverse('api:unified_job_list')
|
||||||
data['activity_stream'] = reverse('api:activity_stream_list')
|
data['activity_stream'] = reverse('api:activity_stream_list')
|
||||||
@@ -684,6 +685,35 @@ class OrganizationActivityStreamList(SubListAPIView):
|
|||||||
# Okay, let it through.
|
# Okay, let it through.
|
||||||
return super(type(self), self).get(request, *args, **kwargs)
|
return super(type(self), self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
class OrganizationNotifiersList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = NotificationTemplateSerializer
|
||||||
|
parent_model = Organization
|
||||||
|
relationship = 'notification_templates'
|
||||||
|
parent_key = 'organization'
|
||||||
|
|
||||||
|
class OrganizationNotificationsAnyList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = NotificationTemplateSerializer
|
||||||
|
parent_model = Organization
|
||||||
|
relationship = 'notification_any'
|
||||||
|
|
||||||
|
class OrganizationNotificationsErrorList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = NotificationTemplateSerializer
|
||||||
|
parent_model = Organization
|
||||||
|
relationship = 'notification_erros'
|
||||||
|
|
||||||
|
class OrganizationNotificationsSuccessList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = NotificationTemplateSerializer
|
||||||
|
parent_model = Organization
|
||||||
|
relationship = 'notification_success'
|
||||||
|
|
||||||
class TeamList(ListCreateAPIView):
|
class TeamList(ListCreateAPIView):
|
||||||
|
|
||||||
model = Team
|
model = Team
|
||||||
@@ -849,6 +879,26 @@ class ProjectActivityStreamList(SubListAPIView):
|
|||||||
return qs.filter(project=parent)
|
return qs.filter(project=parent)
|
||||||
return qs.filter(Q(project=parent) | Q(credential__in=parent.credential))
|
return qs.filter(Q(project=parent) | Q(credential__in=parent.credential))
|
||||||
|
|
||||||
|
class ProjectNotificationsAnyList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = NotificationTemplateSerializer
|
||||||
|
parent_model = Project
|
||||||
|
relationship = 'notification_any'
|
||||||
|
|
||||||
|
class ProjectNotificationsErrorList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = NotificationTemplateSerializer
|
||||||
|
parent_model = Project
|
||||||
|
relationship = 'notification_errors'
|
||||||
|
|
||||||
|
class ProjectNotificationsSuccessList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = NotificationTemplateSerializer
|
||||||
|
parent_model = Project
|
||||||
|
relationship = 'notification_success'
|
||||||
|
|
||||||
class ProjectUpdatesList(SubListAPIView):
|
class ProjectUpdatesList(SubListAPIView):
|
||||||
|
|
||||||
@@ -899,6 +949,12 @@ class ProjectUpdateCancel(RetrieveAPIView):
|
|||||||
else:
|
else:
|
||||||
return self.http_method_not_allowed(request, *args, **kwargs)
|
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):
|
class UserList(ListCreateAPIView):
|
||||||
|
|
||||||
@@ -1725,6 +1781,27 @@ class InventorySourceActivityStreamList(SubListAPIView):
|
|||||||
# Okay, let it through.
|
# Okay, let it through.
|
||||||
return super(type(self), self).get(request, *args, **kwargs)
|
return super(type(self), self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
class InventorySourceNotificationsAnyList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = NotificationTemplateSerializer
|
||||||
|
parent_model = InventorySource
|
||||||
|
relationship = 'notification_any'
|
||||||
|
|
||||||
|
class InventorySourceNotificationsErrorList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = NotificationTemplateSerializer
|
||||||
|
parent_model = InventorySource
|
||||||
|
relationship = 'notification_errors'
|
||||||
|
|
||||||
|
class InventorySourceNotificationsSuccessList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = NotificationTemplateSerializer
|
||||||
|
parent_model = InventorySource
|
||||||
|
relationship = 'notification_success'
|
||||||
|
|
||||||
class InventorySourceHostsList(SubListAPIView):
|
class InventorySourceHostsList(SubListAPIView):
|
||||||
|
|
||||||
model = Host
|
model = Host
|
||||||
@@ -1789,6 +1866,13 @@ class InventoryUpdateCancel(RetrieveAPIView):
|
|||||||
else:
|
else:
|
||||||
return self.http_method_not_allowed(request, *args, **kwargs)
|
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):
|
class JobTemplateList(ListCreateAPIView):
|
||||||
|
|
||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
@@ -1943,6 +2027,27 @@ class JobTemplateActivityStreamList(SubListAPIView):
|
|||||||
# Okay, let it through.
|
# Okay, let it through.
|
||||||
return super(type(self), self).get(request, *args, **kwargs)
|
return super(type(self), self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
class JobTemplateNotificationsAnyList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = NotificationTemplateSerializer
|
||||||
|
parent_model = JobTemplate
|
||||||
|
relationship = 'notification_any'
|
||||||
|
|
||||||
|
class JobTemplateNotificationsErrorList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = NotificationTemplateSerializer
|
||||||
|
parent_model = JobTemplate
|
||||||
|
relationship = 'notification_errors'
|
||||||
|
|
||||||
|
class JobTemplateNotificationsSuccessList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = NotificationTemplateSerializer
|
||||||
|
parent_model = JobTemplate
|
||||||
|
relationship = 'notification_success'
|
||||||
|
|
||||||
class JobTemplateCallback(GenericAPIView):
|
class JobTemplateCallback(GenericAPIView):
|
||||||
|
|
||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
@@ -2129,7 +2234,7 @@ class SystemJobTemplateDetail(RetrieveAPIView):
|
|||||||
class SystemJobTemplateLaunch(GenericAPIView):
|
class SystemJobTemplateLaunch(GenericAPIView):
|
||||||
|
|
||||||
model = SystemJobTemplate
|
model = SystemJobTemplate
|
||||||
# FIXME: Add serializer class to define fields in OPTIONS request!
|
serializer_class = EmptySerializer
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
return Response({})
|
return Response({})
|
||||||
@@ -2276,6 +2381,13 @@ class JobRelaunch(RetrieveAPIView, GenericAPIView):
|
|||||||
headers = {'Location': new_job.get_absolute_url()}
|
headers = {'Location': new_job.get_absolute_url()}
|
||||||
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
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):
|
class BaseJobHostSummariesList(SubListAPIView):
|
||||||
|
|
||||||
model = JobHostSummary
|
model = JobHostSummary
|
||||||
@@ -2926,12 +3038,51 @@ class NotificationTemplateList(ListCreateAPIView):
|
|||||||
serializer_class = NotificationTemplateSerializer
|
serializer_class = NotificationTemplateSerializer
|
||||||
new_in_300 = True
|
new_in_300 = True
|
||||||
|
|
||||||
class NotificationTemplateDetail(RetrieveDestroyAPIView):
|
class NotificationTemplateDetail(RetrieveUpdateDestroyAPIView):
|
||||||
|
|
||||||
model = NotificationTemplate
|
model = NotificationTemplate
|
||||||
serializer_class = NotificationTemplateSerializer
|
serializer_class = NotificationTemplateSerializer
|
||||||
new_in_300 = True
|
new_in_300 = True
|
||||||
|
|
||||||
|
class NotificationTemplateTest(GenericAPIView):
|
||||||
|
|
||||||
|
view_name = 'Notification Template Test'
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = EmptySerializer
|
||||||
|
new_in_300 = True
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
obj = self.get_object()
|
||||||
|
notification = obj.generate_notification("Tower Notification Test", "Ansible Tower Test Notification")
|
||||||
|
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 NotificationTemplateNotificationList(SubListAPIView):
|
||||||
|
|
||||||
|
model = Notification
|
||||||
|
serializer_class = NotificationSerializer
|
||||||
|
parent_model = NotificationTemplate
|
||||||
|
relationship = 'notifications'
|
||||||
|
parent_key = 'notifier'
|
||||||
|
|
||||||
|
class NotificationList(ListAPIView):
|
||||||
|
|
||||||
|
model = Notification
|
||||||
|
serializer_class = NotificationSerializer
|
||||||
|
new_in_300 = True
|
||||||
|
|
||||||
|
class NotificationDetail(RetrieveAPIView):
|
||||||
|
|
||||||
|
model = NotificationTemplate
|
||||||
|
serializer_class = NotificationSerializer
|
||||||
|
new_in_300 = True
|
||||||
|
|
||||||
class ActivityStreamList(SimpleListAPIView):
|
class ActivityStreamList(SimpleListAPIView):
|
||||||
|
|
||||||
model = ActivityStream
|
model = ActivityStream
|
||||||
|
|||||||
@@ -1496,6 +1496,19 @@ class NotificationTemplateAccess(BaseAccess):
|
|||||||
return qs
|
return qs
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
class NotificationAccess(BaseAccess):
|
||||||
|
'''
|
||||||
|
I can see/use a notification if I have permission to
|
||||||
|
'''
|
||||||
|
model = Notification
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = self.model.objects.distinct()
|
||||||
|
if self.user.is_superuser:
|
||||||
|
return qs
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class ActivityStreamAccess(BaseAccess):
|
class ActivityStreamAccess(BaseAccess):
|
||||||
'''
|
'''
|
||||||
I can see activity stream events only when I have permission on all objects included in the event
|
I can see activity stream events only when I have permission on all objects included in the event
|
||||||
@@ -1696,3 +1709,4 @@ register_access(ActivityStream, ActivityStreamAccess)
|
|||||||
register_access(CustomInventoryScript, CustomInventoryScriptAccess)
|
register_access(CustomInventoryScript, CustomInventoryScriptAccess)
|
||||||
register_access(TowerSettings, TowerSettingsAccess)
|
register_access(TowerSettings, TowerSettingsAccess)
|
||||||
register_access(NotificationTemplate, NotificationTemplateAccess)
|
register_access(NotificationTemplate, NotificationTemplateAccess)
|
||||||
|
register_access(Notification, NotificationAccess)
|
||||||
|
|||||||
@@ -61,3 +61,5 @@ activity_stream_registrar.connect(AdHocCommand)
|
|||||||
activity_stream_registrar.connect(Schedule)
|
activity_stream_registrar.connect(Schedule)
|
||||||
activity_stream_registrar.connect(CustomInventoryScript)
|
activity_stream_registrar.connect(CustomInventoryScript)
|
||||||
activity_stream_registrar.connect(TowerSettings)
|
activity_stream_registrar.connect(TowerSettings)
|
||||||
|
activity_stream_registrar.connect(NotificationTemplate)
|
||||||
|
activity_stream_registrar.connect(Notification)
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ class ActivityStream(models.Model):
|
|||||||
ad_hoc_command = models.ManyToManyField("AdHocCommand", blank=True)
|
ad_hoc_command = models.ManyToManyField("AdHocCommand", blank=True)
|
||||||
schedule = models.ManyToManyField("Schedule", blank=True)
|
schedule = models.ManyToManyField("Schedule", blank=True)
|
||||||
custom_inventory_script = models.ManyToManyField("CustomInventoryScript", blank=True)
|
custom_inventory_script = models.ManyToManyField("CustomInventoryScript", blank=True)
|
||||||
|
notification_template = models.ManyToManyField("NotificationTemplate", blank=True)
|
||||||
|
notification = models.ManyToManyField("Notification", blank=True)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('api:activity_stream_detail', args=(self.pk,))
|
return reverse('api:activity_stream_detail', args=(self.pk,))
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from awx.main.utils import encrypt_field
|
|||||||
|
|
||||||
__all__ = ['VarsDictProperty', 'BaseModel', 'CreatedModifiedModel',
|
__all__ = ['VarsDictProperty', 'BaseModel', 'CreatedModifiedModel',
|
||||||
'PasswordFieldsModel', 'PrimordialModel', 'CommonModel',
|
'PasswordFieldsModel', 'PrimordialModel', 'CommonModel',
|
||||||
'CommonModelNameNotUnique',
|
'CommonModelNameNotUnique', 'NotificationFieldsModel',
|
||||||
'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ',
|
'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ',
|
||||||
'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_SCAN',
|
'PERM_INVENTORY_WRITE', 'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_SCAN',
|
||||||
'PERM_INVENTORY_CHECK', 'PERM_JOBTEMPLATE_CREATE', 'JOB_TYPE_CHOICES',
|
'PERM_INVENTORY_CHECK', 'PERM_JOBTEMPLATE_CREATE', 'JOB_TYPE_CHOICES',
|
||||||
@@ -337,3 +337,26 @@ class CommonModelNameNotUnique(PrimordialModel):
|
|||||||
max_length=512,
|
max_length=512,
|
||||||
unique=False,
|
unique=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class NotificationFieldsModel(BaseModel):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
notification_errors = models.ManyToManyField(
|
||||||
|
"NotificationTemplate",
|
||||||
|
blank=True,
|
||||||
|
related_name='%(class)s_notifications_for_errors'
|
||||||
|
)
|
||||||
|
|
||||||
|
notification_success = models.ManyToManyField(
|
||||||
|
"NotificationTemplate",
|
||||||
|
blank=True,
|
||||||
|
related_name='%(class)s_notifications_for_success'
|
||||||
|
)
|
||||||
|
|
||||||
|
notification_any = models.ManyToManyField(
|
||||||
|
"NotificationTemplate",
|
||||||
|
blank=True,
|
||||||
|
related_name='%(class)s_notifications_for_any'
|
||||||
|
)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from awx.main.managers import HostManager
|
|||||||
from awx.main.models.base import * # noqa
|
from awx.main.models.base import * # noqa
|
||||||
from awx.main.models.jobs import Job
|
from awx.main.models.jobs import Job
|
||||||
from awx.main.models.unified_jobs import * # noqa
|
from awx.main.models.unified_jobs import * # noqa
|
||||||
|
from awx.main.models.notifications import NotificationTemplate
|
||||||
from awx.main.utils import ignore_inventory_computed_fields, _inventory_updates
|
from awx.main.utils import ignore_inventory_computed_fields, _inventory_updates
|
||||||
|
|
||||||
__all__ = ['Inventory', 'Host', 'Group', 'InventorySource', 'InventoryUpdate', 'CustomInventoryScript']
|
__all__ = ['Inventory', 'Host', 'Group', 'InventorySource', 'InventoryUpdate', 'CustomInventoryScript']
|
||||||
@@ -1180,6 +1181,15 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def notifiers(self):
|
||||||
|
# Return all notifiers defined on the Project, and on the Organization for each trigger type
|
||||||
|
base_notifiers = NotificationTemplate.objects.filter(active=True)
|
||||||
|
error_notifiers = list(base_notifiers.filter(organization_notifications_for_errors__in=self))
|
||||||
|
success_notifiers = list(base_notifiers.filter(organization_notifications_for_success__in=self))
|
||||||
|
any_notifiers = list(base_notifiers.filter(organization_notifications_for_any__in=self))
|
||||||
|
return dict(error=error_notifiers, success=success_notifiers, any=any_notifiers)
|
||||||
|
|
||||||
def clean_source(self):
|
def clean_source(self):
|
||||||
source = self.source
|
source = self.source
|
||||||
if source and self.group:
|
if source and self.group:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from jsonfield import JSONField
|
|||||||
from awx.main.constants import CLOUD_PROVIDERS
|
from awx.main.constants import CLOUD_PROVIDERS
|
||||||
from awx.main.models.base import * # noqa
|
from awx.main.models.base import * # noqa
|
||||||
from awx.main.models.unified_jobs import * # noqa
|
from awx.main.models.unified_jobs import * # noqa
|
||||||
|
from awx.main.models.notifications import NotificationTemplate
|
||||||
from awx.main.utils import decrypt_field, ignore_inventory_computed_fields
|
from awx.main.utils import decrypt_field, ignore_inventory_computed_fields
|
||||||
from awx.main.utils import emit_websocket_notification
|
from awx.main.utils import emit_websocket_notification
|
||||||
from awx.main.redact import PlainTextCleaner
|
from awx.main.redact import PlainTextCleaner
|
||||||
@@ -330,6 +331,16 @@ class JobTemplate(UnifiedJobTemplate, JobOptions):
|
|||||||
def _can_update(self):
|
def _can_update(self):
|
||||||
return self.can_start_without_user_input()
|
return self.can_start_without_user_input()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def notifiers(self):
|
||||||
|
# Return all notifiers defined on the Job Template, on the Project, and on the Organization for each trigger type
|
||||||
|
# TODO: Currently there is no org fk on project so this will need to be added once that is
|
||||||
|
# available after the rbac pr
|
||||||
|
base_notifiers = NotificationTemplate.objects.filter(active=True)
|
||||||
|
error_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifications_for_errors__in=[self, self.project]))
|
||||||
|
success_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifications_for_success__in=[self, self.project]))
|
||||||
|
any_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifications_for_any__in=[self, self.project]))
|
||||||
|
return dict(error=error_notifiers, success=success_notifiers, any=any_notifiers)
|
||||||
|
|
||||||
class Job(UnifiedJob, JobOptions):
|
class Job(UnifiedJob, JobOptions):
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import logging
|
|||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.core.mail.message import EmailMessage
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.utils.encoding import smart_str
|
||||||
|
|
||||||
from awx.main.models.base import * # noqa
|
from awx.main.models.base import * # noqa
|
||||||
from awx.main.notifications.email_backend import CustomEmailBackend
|
from awx.main.notifications.email_backend import CustomEmailBackend
|
||||||
@@ -17,7 +19,7 @@ from jsonfield import JSONField
|
|||||||
|
|
||||||
logger = logging.getLogger('awx.main.models.notifications')
|
logger = logging.getLogger('awx.main.models.notifications')
|
||||||
|
|
||||||
__all__ = ['NotificationTemplate']
|
__all__ = ['NotificationTemplate', 'Notification']
|
||||||
|
|
||||||
class NotificationTemplate(CommonModel):
|
class NotificationTemplate(CommonModel):
|
||||||
|
|
||||||
@@ -30,6 +32,14 @@ class NotificationTemplate(CommonModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
|
|
||||||
|
organization = models.ForeignKey(
|
||||||
|
'Organization',
|
||||||
|
blank=False,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='notification_templates',
|
||||||
|
)
|
||||||
|
|
||||||
notification_type = models.CharField(
|
notification_type = models.CharField(
|
||||||
max_length = 32,
|
max_length = 32,
|
||||||
choices=NOTIFICATION_TYPE_CHOICES,
|
choices=NOTIFICATION_TYPE_CHOICES,
|
||||||
@@ -42,4 +52,83 @@ class NotificationTemplate(CommonModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def notification_class(self):
|
def notification_class(self):
|
||||||
return CLASS_FOR_NOTIFICATION_TYPE[self.notification_type]
|
return self.CLASS_FOR_NOTIFICATION_TYPE[self.notification_type]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def recipients(self):
|
||||||
|
return self.notification_configuration[self.notification_class.recipient_parameter]
|
||||||
|
|
||||||
|
def generate_notification(self, subject, message):
|
||||||
|
notification = Notification(notifier=self,
|
||||||
|
notification_type=self.notification_type,
|
||||||
|
recipients=smart_str(self.recipients),
|
||||||
|
subject=subject,
|
||||||
|
body=message)
|
||||||
|
notification.save()
|
||||||
|
return notification
|
||||||
|
|
||||||
|
def send(self, subject, body):
|
||||||
|
recipients = self.notification_configuration.pop(self.notification_class.recipient_parameter)
|
||||||
|
sender = self.notification_configuration.pop(self.notification_class.sender_parameter, None)
|
||||||
|
backend_obj = self.notification_class(**self.notification_configuration)
|
||||||
|
notification_obj = EmailMessage(subject, body, sender, recipients)
|
||||||
|
return backend_obj.send_messages([notification_obj])
|
||||||
|
|
||||||
|
class Notification(CreatedModifiedModel):
|
||||||
|
'''
|
||||||
|
A notification event emitted when a Notifier is run
|
||||||
|
'''
|
||||||
|
|
||||||
|
NOTIFICATION_STATE_CHOICES = [
|
||||||
|
('pending', _('Pending')),
|
||||||
|
('successful', _('Successful')),
|
||||||
|
('failed', _('Failed')),
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = 'main'
|
||||||
|
ordering = ('pk',)
|
||||||
|
|
||||||
|
notifier = models.ForeignKey(
|
||||||
|
'NotificationTemplate',
|
||||||
|
related_name='notifications',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=NOTIFICATION_STATE_CHOICES,
|
||||||
|
default='pending',
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
error = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
default='',
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
notifications_sent = models.IntegerField(
|
||||||
|
default=0,
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
notification_type = models.CharField(
|
||||||
|
max_length = 32,
|
||||||
|
choices=NotificationTemplate.NOTIFICATION_TYPE_CHOICES,
|
||||||
|
)
|
||||||
|
recipients = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
default='',
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
subject = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
default='',
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
body = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
default='',
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('api:notification_detail', args=(self.pk,))
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from awx.main.conf import tower_settings
|
|||||||
__all__ = ['Organization', 'Team', 'Permission', 'Profile', 'AuthToken']
|
__all__ = ['Organization', 'Team', 'Permission', 'Profile', 'AuthToken']
|
||||||
|
|
||||||
|
|
||||||
class Organization(CommonModel):
|
class Organization(CommonModel, NotificationFieldsModel):
|
||||||
'''
|
'''
|
||||||
An organization is the basic unit of multi-tenancy divisions
|
An organization is the basic unit of multi-tenancy divisions
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import urlparse
|
|||||||
# Django
|
# Django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.encoding import smart_str, smart_text
|
from django.utils.encoding import smart_str, smart_text
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@@ -20,6 +21,7 @@ from django.utils.timezone import now, make_aware, get_default_timezone
|
|||||||
from awx.lib.compat import slugify
|
from awx.lib.compat import slugify
|
||||||
from awx.main.models.base import * # noqa
|
from awx.main.models.base import * # noqa
|
||||||
from awx.main.models.jobs import Job
|
from awx.main.models.jobs import Job
|
||||||
|
from awx.main.models.notifications import NotificationTemplate
|
||||||
from awx.main.models.unified_jobs import * # noqa
|
from awx.main.models.unified_jobs import * # noqa
|
||||||
from awx.main.utils import update_scm_url
|
from awx.main.utils import update_scm_url
|
||||||
|
|
||||||
@@ -309,6 +311,23 @@ class Project(UnifiedJobTemplate, ProjectOptions):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def notifiers(self):
|
||||||
|
# Return all notifiers defined on the Project, and on the Organization for each trigger type
|
||||||
|
# TODO: Currently there is no org fk on project so this will need to be added back once that is
|
||||||
|
# available after the rbac pr
|
||||||
|
base_notifiers = NotificationTemplate.objects.filter(active=True)
|
||||||
|
# error_notifiers = list(base_notifiers.filter(Q(project_notifications_for_errors__in=self) |
|
||||||
|
# Q(organization_notifications_for_errors__in=self.organization)))
|
||||||
|
# success_notifiers = list(base_notifiers.filter(Q(project_notifications_for_success__in=self) |
|
||||||
|
# Q(organization_notifications_for_success__in=self.organization)))
|
||||||
|
# any_notifiers = list(base_notifiers.filter(Q(project_notifications_for_any__in=self) |
|
||||||
|
# Q(organization_notifications_for_any__in=self.organization)))
|
||||||
|
error_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifications_for_errors=self))
|
||||||
|
success_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifications_for_success=self))
|
||||||
|
any_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifications_for_any=self))
|
||||||
|
return dict(error=error_notifiers, success=success_notifiers, any=any_notifiers)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('api:project_detail', args=(self.pk,))
|
return reverse('api:project_detail', args=(self.pk,))
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from djcelery.models import TaskMeta
|
|||||||
# AWX
|
# AWX
|
||||||
from awx.main.models.base import * # noqa
|
from awx.main.models.base import * # noqa
|
||||||
from awx.main.models.schedules import Schedule
|
from awx.main.models.schedules import Schedule
|
||||||
|
from awx.main.models.notifications import Notification
|
||||||
from awx.main.utils import decrypt_field, emit_websocket_notification, _inventory_updates
|
from awx.main.utils import decrypt_field, emit_websocket_notification, _inventory_updates
|
||||||
from awx.main.redact import UriCleaner
|
from awx.main.redact import UriCleaner
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ logger = logging.getLogger('awx.main.models.unified_jobs')
|
|||||||
CAN_CANCEL = ('new', 'pending', 'waiting', 'running')
|
CAN_CANCEL = ('new', 'pending', 'waiting', 'running')
|
||||||
|
|
||||||
|
|
||||||
class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique):
|
class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, NotificationFieldsModel):
|
||||||
'''
|
'''
|
||||||
Concrete base class for unified job templates.
|
Concrete base class for unified job templates.
|
||||||
'''
|
'''
|
||||||
@@ -297,6 +298,14 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique):
|
|||||||
'''
|
'''
|
||||||
return kwargs # Override if needed in subclass.
|
return kwargs # Override if needed in subclass.
|
||||||
|
|
||||||
|
@property
|
||||||
|
def notifiers(self):
|
||||||
|
'''
|
||||||
|
Return notifiers relevant to this Unified Job Template
|
||||||
|
'''
|
||||||
|
# NOTE: Derived classes should implement
|
||||||
|
return NotificationTemplate.objects.none()
|
||||||
|
|
||||||
def create_unified_job(self, **kwargs):
|
def create_unified_job(self, **kwargs):
|
||||||
'''
|
'''
|
||||||
Create a new unified job based on this unified job template.
|
Create a new unified job based on this unified job template.
|
||||||
@@ -385,6 +394,11 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
|||||||
editable=False,
|
editable=False,
|
||||||
related_name='%(class)s_blocked_jobs+',
|
related_name='%(class)s_blocked_jobs+',
|
||||||
)
|
)
|
||||||
|
notifications = models.ManyToManyField(
|
||||||
|
'Notification',
|
||||||
|
editable=False,
|
||||||
|
related_name='%(class)s_notifications',
|
||||||
|
)
|
||||||
cancel_flag = models.BooleanField(
|
cancel_flag = models.BooleanField(
|
||||||
blank=True,
|
blank=True,
|
||||||
default=False,
|
default=False,
|
||||||
|
|||||||
@@ -12,5 +12,9 @@ class CustomEmailBackend(EmailBackend):
|
|||||||
"username": {"label": "Username", "type": "string"},
|
"username": {"label": "Username", "type": "string"},
|
||||||
"password": {"label": "Password", "type": "password"},
|
"password": {"label": "Password", "type": "password"},
|
||||||
"use_tls": {"label": "Use TLS", "type": "bool"},
|
"use_tls": {"label": "Use TLS", "type": "bool"},
|
||||||
"use_ssl": {"label": "Use SSL", "type": "bool"}}
|
"use_ssl": {"label": "Use SSL", "type": "bool"},
|
||||||
|
"sender": {"label": "Sender Email", "type": "string"},
|
||||||
|
"recipients": {"label": "Recipient List", "type": "list"}}
|
||||||
|
recipient_parameter = "recipients"
|
||||||
|
sender_parameter = "sender"
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ logger = logging.getLogger('awx.main.notifications.slack_backend')
|
|||||||
|
|
||||||
class SlackBackend(BaseEmailBackend):
|
class SlackBackend(BaseEmailBackend):
|
||||||
|
|
||||||
init_parameters = {"token": {"label": "Token", "type": "password"}}
|
init_parameters = {"token": {"label": "Token", "type": "password"},
|
||||||
|
"channels": {"label": "Destination Channels", "type": "list"}}
|
||||||
|
recipient_parameter = "channels"
|
||||||
|
sender_parameter = None
|
||||||
|
|
||||||
def __init__(self, token, fail_silently=False, **kwargs):
|
def __init__(self, token, fail_silently=False, **kwargs):
|
||||||
super(SlackBackend, self).__init__(fail_silently=fail_silently)
|
super(SlackBackend, self).__init__(fail_silently=fail_silently)
|
||||||
@@ -37,8 +40,9 @@ class SlackBackend(BaseEmailBackend):
|
|||||||
sent_messages = 0
|
sent_messages = 0
|
||||||
for m in messages:
|
for m in messages:
|
||||||
try:
|
try:
|
||||||
self.connection.rtm_send_message(m.to, m.body)
|
for r in m.recipients():
|
||||||
sent_messages += 1
|
self.connection.rtm_send_message(r, m.body)
|
||||||
|
sent_messages += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if not self.fail_silently:
|
if not self.fail_silently:
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ class TwilioBackend(BaseEmailBackend):
|
|||||||
|
|
||||||
init_parameters = {"account_sid": {"label": "Account SID", "type": "string"},
|
init_parameters = {"account_sid": {"label": "Account SID", "type": "string"},
|
||||||
"account_token": {"label": "Account Token", "type": "password"},
|
"account_token": {"label": "Account Token", "type": "password"},
|
||||||
"from_phone": {"label": "Source Phone Number", "type": "string"}}
|
"from_number": {"label": "Source Phone Number", "type": "string"},
|
||||||
|
"to_numbers": {"label": "Destination SMS Numbers", "type": "list"}}
|
||||||
|
recipient_parameter = "to_numbers"
|
||||||
|
sender_parameter = "from_number"
|
||||||
|
|
||||||
def __init__(self, account_sid, account_token, from_phone, fail_silently=False, **kwargs):
|
def __init__(self, account_sid, account_token, from_phone, fail_silently=False, **kwargs):
|
||||||
super(TwilioBackend, self).__init__(fail_silently=fail_silently)
|
super(TwilioBackend, self).__init__(fail_silently=fail_silently)
|
||||||
@@ -34,7 +37,7 @@ class TwilioBackend(BaseEmailBackend):
|
|||||||
try:
|
try:
|
||||||
connection.messages.create(
|
connection.messages.create(
|
||||||
to=m.to,
|
to=m.to,
|
||||||
from_=self.from_phone,
|
from_=m.from_email,
|
||||||
body=m.body)
|
body=m.body)
|
||||||
sent_messages += 1
|
sent_messages += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -307,6 +307,8 @@ model_serializer_mapping = {
|
|||||||
Job: JobSerializer,
|
Job: JobSerializer,
|
||||||
AdHocCommand: AdHocCommandSerializer,
|
AdHocCommand: AdHocCommandSerializer,
|
||||||
TowerSettings: TowerSettingsSerializer,
|
TowerSettings: TowerSettingsSerializer,
|
||||||
|
NotificationTemplate: NotificationTemplateSerializer,
|
||||||
|
Notification: NotificationSerializer,
|
||||||
}
|
}
|
||||||
|
|
||||||
def activity_stream_create(sender, instance, created, **kwargs):
|
def activity_stream_create(sender, instance, created, **kwargs):
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ from awx.fact.utils.connection import test_mongo_connection
|
|||||||
|
|
||||||
__all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate',
|
__all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate',
|
||||||
'RunAdHocCommand', 'handle_work_error', 'handle_work_success',
|
'RunAdHocCommand', 'handle_work_error', 'handle_work_success',
|
||||||
'update_inventory_computed_fields']
|
'update_inventory_computed_fields', 'send_notifications']
|
||||||
|
|
||||||
HIDDEN_PASSWORD = '**********'
|
HIDDEN_PASSWORD = '**********'
|
||||||
|
|
||||||
@@ -65,6 +65,26 @@ Try upgrading OpenSSH or providing your private key in an different format. \
|
|||||||
|
|
||||||
logger = logging.getLogger('awx.main.tasks')
|
logger = logging.getLogger('awx.main.tasks')
|
||||||
|
|
||||||
|
@task()
|
||||||
|
def send_notifications(notification_list, job_id=None):
|
||||||
|
if not isinstance(notification_list, list):
|
||||||
|
raise TypeError("notification_list should be of type list")
|
||||||
|
for notification_id in notification_list:
|
||||||
|
notification = Notification.objects.get(id=notification_id)
|
||||||
|
try:
|
||||||
|
sent = notification.notifier.send(notification.subject, notification.body)
|
||||||
|
notification.status = "successful"
|
||||||
|
notification.notifications_sent = sent
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Send Notification Failed {}".format(e))
|
||||||
|
notification.status = "failed"
|
||||||
|
notification.error = str(e)
|
||||||
|
finally:
|
||||||
|
notification.save()
|
||||||
|
if job_id is not None:
|
||||||
|
j = UnifiedJob.objects.get(id=job_id)
|
||||||
|
j.notifications.add(notification)
|
||||||
|
|
||||||
@task()
|
@task()
|
||||||
def bulk_inventory_element_delete(inventory, hosts=[], groups=[]):
|
def bulk_inventory_element_delete(inventory, hosts=[], groups=[]):
|
||||||
from awx.main.signals import disable_activity_stream
|
from awx.main.signals import disable_activity_stream
|
||||||
@@ -162,12 +182,41 @@ def mongodb_control(cmd):
|
|||||||
|
|
||||||
@task(bind=True)
|
@task(bind=True)
|
||||||
def handle_work_success(self, result, task_actual):
|
def handle_work_success(self, result, task_actual):
|
||||||
# TODO: Perform Notification tasks
|
if task_actual['type'] == 'project_update':
|
||||||
pass
|
instance = ProjectUpdate.objects.get(id=task_actual['id'])
|
||||||
|
instance_name = instance.name
|
||||||
|
notifiers = instance.project.notifiers
|
||||||
|
friendly_name = "Project Update"
|
||||||
|
elif task_actual['type'] == 'inventory_update':
|
||||||
|
instance = InventoryUpdate.objects.get(id=task_actual['id'])
|
||||||
|
instance_name = instance.name
|
||||||
|
notifiers = instance.inventory_source.notifiers
|
||||||
|
friendly_name = "Inventory Update"
|
||||||
|
elif task_actual['type'] == 'job':
|
||||||
|
instance = Job.objects.get(id=task_actual['id'])
|
||||||
|
instance_name = instance.job_template.name
|
||||||
|
notifiers = instance.job_template.notifiers
|
||||||
|
friendly_name = "Job"
|
||||||
|
elif task_actual['type'] == 'ad_hoc_command':
|
||||||
|
instance = AdHocCommand.objects.get(id=task_actual['id'])
|
||||||
|
instance_name = instance.module_name
|
||||||
|
notifiers = [] # TODO: Ad-hoc commands need to notify someone
|
||||||
|
friendly_name = "AdHoc Command"
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
notification_subject = "{} #{} '{}' succeeded on Ansible Tower".format(friendly_name,
|
||||||
|
task_actual['id'],
|
||||||
|
instance_name)
|
||||||
|
notification_body = "{} #{} '{}' succeeded on Ansible Tower\nTo view the output: {}".format(friendly_name,
|
||||||
|
task_actual['id'],
|
||||||
|
instance_name,
|
||||||
|
instance.get_absolute_url())
|
||||||
|
send_notifications.delay([n.generate_notification(notification_subject, notification_body)
|
||||||
|
for n in notifiers.get('success', []) + notifiers.get('any', [])],
|
||||||
|
job_id=task_actual['id'])
|
||||||
|
|
||||||
@task(bind=True)
|
@task(bind=True)
|
||||||
def handle_work_error(self, task_id, subtasks=None):
|
def handle_work_error(self, task_id, subtasks=None):
|
||||||
# TODO: Perform Notification tasks
|
|
||||||
print('Executing error task id %s, subtasks: %s' %
|
print('Executing error task id %s, subtasks: %s' %
|
||||||
(str(self.request.id), str(subtasks)))
|
(str(self.request.id), str(subtasks)))
|
||||||
first_task = None
|
first_task = None
|
||||||
@@ -180,15 +229,23 @@ def handle_work_error(self, task_id, subtasks=None):
|
|||||||
if each_task['type'] == 'project_update':
|
if each_task['type'] == 'project_update':
|
||||||
instance = ProjectUpdate.objects.get(id=each_task['id'])
|
instance = ProjectUpdate.objects.get(id=each_task['id'])
|
||||||
instance_name = instance.name
|
instance_name = instance.name
|
||||||
|
notifiers = instance.project.notifiers
|
||||||
|
friendly_name = "Project Update"
|
||||||
elif each_task['type'] == 'inventory_update':
|
elif each_task['type'] == 'inventory_update':
|
||||||
instance = InventoryUpdate.objects.get(id=each_task['id'])
|
instance = InventoryUpdate.objects.get(id=each_task['id'])
|
||||||
instance_name = instance.name
|
instance_name = instance.name
|
||||||
|
notifiers = instance.inventory_source.notifiers
|
||||||
|
friendly_name = "Inventory Update"
|
||||||
elif each_task['type'] == 'job':
|
elif each_task['type'] == 'job':
|
||||||
instance = Job.objects.get(id=each_task['id'])
|
instance = Job.objects.get(id=each_task['id'])
|
||||||
instance_name = instance.job_template.name
|
instance_name = instance.job_template.name
|
||||||
|
notifiers = instance.job_template.notifiers
|
||||||
|
friendly_name = "Job"
|
||||||
elif each_task['type'] == 'ad_hoc_command':
|
elif each_task['type'] == 'ad_hoc_command':
|
||||||
instance = AdHocCommand.objects.get(id=each_task['id'])
|
instance = AdHocCommand.objects.get(id=each_task['id'])
|
||||||
instance_name = instance.module_name
|
instance_name = instance.module_name
|
||||||
|
notifiers = []
|
||||||
|
friendly_name = "AdHoc Command"
|
||||||
else:
|
else:
|
||||||
# Unknown task type
|
# Unknown task type
|
||||||
break
|
break
|
||||||
@@ -197,6 +254,7 @@ def handle_work_error(self, task_id, subtasks=None):
|
|||||||
first_task_id = instance.id
|
first_task_id = instance.id
|
||||||
first_task_type = each_task['type']
|
first_task_type = each_task['type']
|
||||||
first_task_name = instance_name
|
first_task_name = instance_name
|
||||||
|
first_task_friendly_name = friendly_name
|
||||||
if instance.celery_task_id != task_id:
|
if instance.celery_task_id != task_id:
|
||||||
instance.status = 'failed'
|
instance.status = 'failed'
|
||||||
instance.failed = True
|
instance.failed = True
|
||||||
@@ -204,6 +262,17 @@ def handle_work_error(self, task_id, subtasks=None):
|
|||||||
(first_task_type, first_task_name, first_task_id)
|
(first_task_type, first_task_name, first_task_id)
|
||||||
instance.save()
|
instance.save()
|
||||||
instance.socketio_emit_status("failed")
|
instance.socketio_emit_status("failed")
|
||||||
|
notification_subject = "{} #{} '{}' failed on Ansible Tower".format(first_task_friendly_name,
|
||||||
|
first_task_id,
|
||||||
|
first_task_name)
|
||||||
|
notification_body = "{} #{} '{}' failed on Ansible Tower\nTo view the output: {}".format(first_task_friendly_name,
|
||||||
|
first_task_id,
|
||||||
|
first_task_name,
|
||||||
|
first_task.get_absolute_url())
|
||||||
|
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
|
||||||
|
for n in notifiers.get('error', []) + notifiers.get('any', [])],
|
||||||
|
job_id=first_task_id)
|
||||||
|
|
||||||
|
|
||||||
@task()
|
@task()
|
||||||
def update_inventory_computed_fields(inventory_id, should_update_hosts=True):
|
def update_inventory_computed_fields(inventory_id, should_update_hosts=True):
|
||||||
|
|||||||
Reference in New Issue
Block a user