Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions awx/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1392,6 +1392,7 @@ def get_related(self, obj):
notification_templates_success=self.reverse('api:organization_notification_templates_success_list', kwargs={'pk': obj.pk}),
notification_templates_error=self.reverse('api:organization_notification_templates_error_list', kwargs={'pk': obj.pk}),
notification_templates_approvals=self.reverse('api:organization_notification_templates_approvals_list', kwargs={'pk': obj.pk}),
notification_templates_changed=self.reverse('api:organization_notification_templates_changed_list', kwargs={'pk': obj.pk}),
object_roles=self.reverse('api:organization_object_roles_list', kwargs={'pk': obj.pk}),
access_list=self.reverse('api:organization_access_list', kwargs={'pk': obj.pk}),
instance_groups=self.reverse('api:organization_instance_groups_list', kwargs={'pk': obj.pk}),
Expand Down Expand Up @@ -3482,6 +3483,7 @@ def get_related(self, obj):
notification_templates_started=self.reverse('api:job_template_notification_templates_started_list', kwargs={'pk': obj.pk}),
notification_templates_success=self.reverse('api:job_template_notification_templates_success_list', kwargs={'pk': obj.pk}),
notification_templates_error=self.reverse('api:job_template_notification_templates_error_list', kwargs={'pk': obj.pk}),
notification_templates_changed=self.reverse('api:job_template_notification_templates_changed_list', kwargs={'pk': obj.pk}),
access_list=self.reverse('api:job_template_access_list', kwargs={'pk': obj.pk}),
survey_spec=self.reverse('api:job_template_survey_spec', kwargs={'pk': obj.pk}),
labels=self.reverse('api:job_template_label_list', kwargs={'pk': obj.pk}),
Expand Down Expand Up @@ -5393,8 +5395,8 @@ def check_messages(messages):
error_list.append(_("Expected dict for 'messages' field, found {}".format(type(messages))))
else:
for event in messages:
if event not in ('started', 'success', 'error', 'workflow_approval'):
error_list.append(_("Event '{}' invalid, must be one of 'started', 'success', 'error', or 'workflow_approval'").format(event))
if event not in ('started', 'success', 'error', 'changed', 'workflow_approval'):
error_list.append(_("Event '{}' invalid, must be one of 'started', 'success', 'error', 'changed', or 'workflow_approval'").format(event))
continue
event_messages = messages[event]
if event_messages is None:
Expand Down
6 changes: 6 additions & 0 deletions awx/api/urls/job_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
JobTemplateNotificationTemplatesErrorList,
JobTemplateNotificationTemplatesStartedList,
JobTemplateNotificationTemplatesSuccessList,
JobTemplateNotificationTemplatesChangedList,
JobTemplateInstanceGroupsList,
JobTemplateAccessList,
JobTemplateObjectRolesList,
Expand Down Expand Up @@ -49,6 +50,11 @@
JobTemplateNotificationTemplatesSuccessList.as_view(),
name='job_template_notification_templates_success_list',
),
path(
'<int:pk>/notification_templates_changed/',
JobTemplateNotificationTemplatesChangedList.as_view(),
name='job_template_notification_templates_changed_list',
),
path('<int:pk>/instance_groups/', JobTemplateInstanceGroupsList.as_view(), name='job_template_instance_groups_list'),
path('<int:pk>/access_list/', JobTemplateAccessList.as_view(), name='job_template_access_list'),
path('<int:pk>/object_roles/', JobTemplateObjectRolesList.as_view(), name='job_template_object_roles_list'),
Expand Down
6 changes: 6 additions & 0 deletions awx/api/urls/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
OrganizationNotificationTemplatesStartedList,
OrganizationNotificationTemplatesSuccessList,
OrganizationNotificationTemplatesApprovalList,
OrganizationNotificationTemplatesChangedList,
OrganizationInstanceGroupsList,
OrganizationGalaxyCredentialsList,
OrganizationObjectRolesList,
Expand Down Expand Up @@ -61,6 +62,11 @@
OrganizationNotificationTemplatesApprovalList.as_view(),
name='organization_notification_templates_approvals_list',
),
path(
'<int:pk>/notification_templates_changed/',
OrganizationNotificationTemplatesChangedList.as_view(),
name='organization_notification_templates_changed_list',
),
path('<int:pk>/instance_groups/', OrganizationInstanceGroupsList.as_view(), name='organization_instance_groups_list'),
path('<int:pk>/galaxy_credentials/', OrganizationGalaxyCredentialsList.as_view(), name='organization_galaxy_credentials_list'),
path('<int:pk>/object_roles/', OrganizationObjectRolesList.as_view(), name='organization_object_roles_list'),
Expand Down
4 changes: 4 additions & 0 deletions awx/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2708,6 +2708,10 @@ class JobTemplateNotificationTemplatesSuccessList(JobTemplateNotificationTemplat
relationship = 'notification_templates_success'


class JobTemplateNotificationTemplatesChangedList(JobTemplateNotificationTemplatesAnyList):
relationship = 'notification_templates_changed'


class JobTemplateCredentialsList(SubListCreateAttachDetachAPIView):
model = models.Credential
serializer_class = serializers.CredentialSerializer
Expand Down
4 changes: 4 additions & 0 deletions awx/api/views/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ class OrganizationNotificationTemplatesApprovalList(OrganizationNotificationTemp
relationship = 'notification_templates_approvals'


class OrganizationNotificationTemplatesChangedList(OrganizationNotificationTemplatesAnyList):
relationship = 'notification_templates_changed'


class OrganizationInstanceGroupsList(OrganizationInstanceGroupMembershipMixin, SubListAttachDetachAPIView):
model = InstanceGroup
serializer_class = InstanceGroupSerializer
Expand Down
20 changes: 20 additions & 0 deletions awx/main/migrations/0210_notification_templates_changed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
('main', '0209_cleanup_dab_rbac_leftovers'),
]

operations = [
migrations.AddField(
model_name='jobtemplate',
name='notification_templates_changed',
field=models.ManyToManyField(blank=True, related_name='%(class)s_notification_templates_for_changed', to='main.notificationtemplate'),
),
migrations.AddField(
model_name='organization',
name='notification_templates_changed',
field=models.ManyToManyField(blank=True, related_name='%(class)s_notification_templates_for_changed', to='main.notificationtemplate'),
),
]
12 changes: 11 additions & 1 deletion awx/main/models/ad_hoc_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ def get_ui_url(self):
@property
def notification_templates(self):
all_orgs = {h.inventory.organization for h in self.hosts.all()}
active_templates = dict(error=set(), success=set(), started=set())
active_templates = dict(error=set(), success=set(), started=set(), changed=set())
base_notification_templates = NotificationTemplate.objects
for org in all_orgs:
for templ in base_notification_templates.filter(organization_notification_templates_for_errors=org):
Expand All @@ -175,9 +175,12 @@ def notification_templates(self):
active_templates['success'].add(templ)
for templ in base_notification_templates.filter(organization_notification_templates_for_started=org):
active_templates['started'].add(templ)
for templ in base_notification_templates.filter(organization_notification_templates_for_changed=org):
active_templates['changed'].add(templ)
active_templates['error'] = list(active_templates['error'])
active_templates['success'] = list(active_templates['success'])
active_templates['started'] = list(active_templates['started'])
active_templates['changed'] = list(active_templates['changed'])
return active_templates

def get_passwords_needed_to_start(self):
Expand Down Expand Up @@ -251,3 +254,10 @@ def get_notification_templates(self):

def get_notification_friendly_name(self):
return "AdHoc Command"

def has_changes(self):
"""
Whether the command reported a change on any host. An ad hoc command has no
JobHostSummary rows, so this reads the per host events instead.
"""
return self.ad_hoc_command_events.filter(changed=True).exists()
25 changes: 24 additions & 1 deletion awx/main/models/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,11 @@ class Meta:
"groups will be applied."
),
)
notification_templates_changed = models.ManyToManyField(
"NotificationTemplate",
blank=True,
related_name='%(class)s_notification_templates_for_changed',
)

@classmethod
def _get_unified_job_class(cls):
Expand Down Expand Up @@ -618,6 +623,9 @@ def notification_templates(self):
success_notification_templates = list(
base_notification_templates.filter(unifiedjobtemplate_notification_templates_for_success__in=[self, self.project])
)
# Changes are reported by the job itself, so this trigger only exists on the job
# template and on its organization, not on the project.
changed_notification_templates = list(base_notification_templates.filter(jobtemplate_notification_templates_for_changed__in=[self]))
# Get Organization NotificationTemplates
if self.organization is not None:
error_notification_templates = set(
Expand All @@ -629,7 +637,15 @@ def notification_templates(self):
success_notification_templates = set(
success_notification_templates + list(base_notification_templates.filter(organization_notification_templates_for_success=self.organization))
)
return dict(error=list(error_notification_templates), started=list(started_notification_templates), success=list(success_notification_templates))
changed_notification_templates = set(
changed_notification_templates + list(base_notification_templates.filter(organization_notification_templates_for_changed=self.organization))
)
return dict(
error=list(error_notification_templates),
started=list(started_notification_templates),
success=list(success_notification_templates),
changed=list(changed_notification_templates),
)

'''
RelatedJobsMixin
Expand Down Expand Up @@ -929,6 +945,13 @@ def get_notification_templates(self):
def get_notification_friendly_name(self):
return "Job"

def has_changes(self):
"""
Whether the run reported a change on any host. Check mode counts, which is what
makes this useful for a compliance playbook that is only meant to report drift.
"""
return self.job_host_summaries.filter(changed__gt=0).exists()

def get_hosts_for_fact_cache(self):
"""
Builds the queryset to use for writing or finalizing the fact cache
Expand Down
21 changes: 17 additions & 4 deletions awx/main/models/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class Meta:
notification_configuration = prevent_search(models.JSONField(default=dict))

def default_messages():
return {'started': None, 'success': None, 'error': None, 'workflow_approval': None}
return {'started': None, 'success': None, 'error': None, 'changed': None, 'workflow_approval': None}

messages = models.JSONField(null=True, blank=True, default=default_messages, help_text=_('Optional custom messages for notification template.'))

Expand Down Expand Up @@ -244,7 +244,7 @@ def get_absolute_url(self, request=None):


class JobNotificationMixin(object):
STATUS_TO_TEMPLATE_TYPE = {'succeeded': 'success', 'running': 'started', 'failed': 'error'}
STATUS_TO_TEMPLATE_TYPE = {'succeeded': 'success', 'running': 'started', 'failed': 'error', 'changed': 'changed'}
# Maximum number of host names exposed in the notification context to keep
# payloads bounded for jobs run against very large inventories.
HOST_LIST_MAX = 1000
Expand Down Expand Up @@ -462,6 +462,13 @@ def get_notification_templates(self):
def get_notification_friendly_name(self):
raise RuntimeError("Define me")

def has_changes(self):
"""
Whether the run reported a change. Only job types that record per host results can
answer this, so everything else never triggers the changed notifications.
"""
return False

def notification_data(self):
raise RuntimeError("Define me")

Expand Down Expand Up @@ -520,8 +527,8 @@ def build_notification_message(self, nt, status):
def send_notification_templates(self, status):
from awx.main.tasks.system import send_notifications # avoid circular import

if status not in ['running', 'succeeded', 'failed']:
raise ValueError(_("status must be either running, succeeded or failed"))
if status not in ['running', 'succeeded', 'failed', 'changed']:
raise ValueError(_("status must be either running, succeeded, failed or changed"))
try:
notification_templates = self.get_notification_templates()
except Exception:
Expand All @@ -531,6 +538,12 @@ def send_notification_templates(self, status):
if not notification_templates:
return

# A run that changed something notifies the templates set up for changes on top of
# the ones for how it ended, so that a playbook run in check mode that reports drift
# is notified even though it succeeded.
if status in ('succeeded', 'failed') and notification_templates.get('changed') and self.has_changes():
self.send_notification_templates('changed')

for nt in set(notification_templates.get(self.STATUS_TO_TEMPLATE_TYPE[status], [])):
msg, body = self.build_notification_message(nt, status)

Expand Down
1 change: 1 addition & 0 deletions awx/main/models/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Meta:
help_text=_('Maximum number of hosts allowed to be managed by this organization.'),
)
notification_templates_approvals = models.ManyToManyField("NotificationTemplate", blank=True, related_name='%(class)s_notification_templates_for_approvals')
notification_templates_changed = models.ManyToManyField("NotificationTemplate", blank=True, related_name='%(class)s_notification_templates_for_changed')
default_environment = models.ForeignKey(
'ExecutionEnvironment',
null=True,
Expand Down
6 changes: 6 additions & 0 deletions awx/main/notifications/custom_notification_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ class CustomNotificationBase(object):
DEFAULT_MSG = "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' {{ job.status }}: {{ url }}"
DEFAULT_BODY = "{{ job_friendly_name }} #{{ job.id }} had status {{ job.status }}, view details at {{ url }}\n\n{{ job_metadata }}"

DEFAULT_CHANGED_MSG = "{{ job_friendly_name }} #{{ job.id }} '{{ job.name }}' reported changes: {{ url }}"
DEFAULT_CHANGED_BODY = (
"{{ job_friendly_name }} #{{ job.id }} reported changes and had status {{ job.status }}, view details at {{ url }}\n\n{{ job_metadata }}"
)

DEFAULT_APPROVAL_RUNNING_MSG = 'The approval node "{{ approval_node_name }}" needs review. This node can be viewed at: {{ workflow_url }}'
DEFAULT_APPROVAL_RUNNING_BODY = (
'The approval node "{{ approval_node_name }}" needs review. This approval node can be viewed at: {{ workflow_url }}'
Expand All @@ -27,6 +32,7 @@ class CustomNotificationBase(object):
"started": {"message": DEFAULT_MSG, "body": None},
"success": {"message": DEFAULT_MSG, "body": None},
"error": {"message": DEFAULT_MSG, "body": None},
"changed": {"message": DEFAULT_CHANGED_MSG, "body": None},
"workflow_approval": {
"running": {"message": DEFAULT_APPROVAL_RUNNING_MSG, "body": None},
"approved": {"message": DEFAULT_APPROVAL_APPROVED_MSG, "body": None},
Expand Down
4 changes: 4 additions & 0 deletions awx/main/notifications/email_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
DEFAULT_MSG = CustomNotificationBase.DEFAULT_MSG
DEFAULT_BODY = CustomNotificationBase.DEFAULT_BODY

DEFAULT_CHANGED_MSG = CustomNotificationBase.DEFAULT_CHANGED_MSG
DEFAULT_CHANGED_BODY = CustomNotificationBase.DEFAULT_CHANGED_BODY

DEFAULT_APPROVAL_RUNNING_MSG = CustomNotificationBase.DEFAULT_APPROVAL_RUNNING_MSG
DEFAULT_APPROVAL_RUNNING_BODY = CustomNotificationBase.DEFAULT_APPROVAL_RUNNING_BODY

Expand Down Expand Up @@ -40,6 +43,7 @@ class CustomEmailBackend(EmailBackend, CustomNotificationBase):
"started": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"success": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"error": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"changed": {"message": DEFAULT_CHANGED_MSG, "body": DEFAULT_CHANGED_BODY},
"workflow_approval": {
"running": {"message": DEFAULT_APPROVAL_RUNNING_MSG, "body": DEFAULT_APPROVAL_RUNNING_BODY},
"approved": {"message": DEFAULT_APPROVAL_APPROVED_MSG, "body": DEFAULT_APPROVAL_APPROVED_BODY},
Expand Down
2 changes: 2 additions & 0 deletions awx/main/notifications/grafana_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from awx.main.notifications.custom_notification_base import CustomNotificationBase

DEFAULT_MSG = CustomNotificationBase.DEFAULT_MSG
DEFAULT_CHANGED_MSG = CustomNotificationBase.DEFAULT_CHANGED_MSG

DEFAULT_APPROVAL_RUNNING_MSG = CustomNotificationBase.DEFAULT_APPROVAL_RUNNING_MSG
DEFAULT_APPROVAL_RUNNING_BODY = CustomNotificationBase.DEFAULT_APPROVAL_RUNNING_BODY
Expand All @@ -41,6 +42,7 @@ class GrafanaBackend(AWXBaseEmailBackend, CustomNotificationBase):
"started": {"body": DEFAULT_BODY, "message": DEFAULT_MSG},
"success": {"body": DEFAULT_BODY, "message": DEFAULT_MSG},
"error": {"body": DEFAULT_BODY, "message": DEFAULT_MSG},
"changed": {"body": DEFAULT_BODY, "message": DEFAULT_CHANGED_MSG},
"workflow_approval": {
"running": {"message": DEFAULT_APPROVAL_RUNNING_MSG, "body": DEFAULT_APPROVAL_RUNNING_BODY},
"approved": {"message": DEFAULT_APPROVAL_APPROVED_MSG, "body": DEFAULT_APPROVAL_APPROVED_BODY},
Expand Down
2 changes: 2 additions & 0 deletions awx/main/notifications/pagerduty_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from awx.main.notifications.custom_notification_base import CustomNotificationBase

DEFAULT_MSG = CustomNotificationBase.DEFAULT_MSG
DEFAULT_CHANGED_MSG = CustomNotificationBase.DEFAULT_CHANGED_MSG

DEFAULT_APPROVAL_RUNNING_MSG = CustomNotificationBase.DEFAULT_APPROVAL_RUNNING_MSG
DEFAULT_APPROVAL_RUNNING_BODY = CustomNotificationBase.DEFAULT_APPROVAL_RUNNING_BODY
Expand Down Expand Up @@ -43,6 +44,7 @@ class PagerDutyBackend(AWXBaseEmailBackend, CustomNotificationBase):
"started": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"success": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"error": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"changed": {"message": DEFAULT_CHANGED_MSG, "body": DEFAULT_BODY},
"workflow_approval": {
"running": {"message": DEFAULT_APPROVAL_RUNNING_MSG, "body": DEFAULT_APPROVAL_RUNNING_BODY},
"approved": {"message": DEFAULT_APPROVAL_APPROVED_MSG, "body": DEFAULT_APPROVAL_APPROVED_BODY},
Expand Down
1 change: 1 addition & 0 deletions awx/main/notifications/webhook_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class WebhookBackend(AWXBaseEmailBackend, CustomNotificationBase):
"started": {"body": DEFAULT_BODY},
"success": {"body": DEFAULT_BODY},
"error": {"body": DEFAULT_BODY},
"changed": {"body": DEFAULT_BODY},
"workflow_approval": {
"running": {"body": '{"body": "The approval node \\"{{ approval_node_name }}\\" needs review. This node can be viewed at: {{ workflow_url }}"}'},
"approved": {"body": '{"body": "The approval node \\"{{ approval_node_name }}\\" was approved. {{ workflow_url }}"}'},
Expand Down
Loading