diff --git a/awx/main/models/ad_hoc_commands.py b/awx/main/models/ad_hoc_commands.py index da9aaf740b..5c4a729c3e 100644 --- a/awx/main/models/ad_hoc_commands.py +++ b/awx/main/models/ad_hoc_commands.py @@ -23,13 +23,14 @@ from awx.main.models.base import * # noqa from awx.main.models.unified_jobs import * # noqa from awx.main.utils import decrypt_field from awx.main.conf import tower_settings +from awx.main.models.notifications import JobNotificationMixin logger = logging.getLogger('awx.main.models.ad_hoc_commands') __all__ = ['AdHocCommand', 'AdHocCommandEvent'] -class AdHocCommand(UnifiedJob): +class AdHocCommand(UnifiedJob, JobNotificationMixin): class Meta(object): app_label = 'main' @@ -237,6 +238,14 @@ class AdHocCommand(UnifiedJob): update_fields.append('name') super(AdHocCommand, self).save(*args, **kwargs) + ''' + JobNotificationMixin + ''' + def get_notification_templates(self): + return self.notification_templates + + def get_notification_friendly_name(self): + return "AdHoc Command" class AdHocCommandEvent(CreatedModifiedModel): ''' diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 8dde9f3b3b..0955a28667 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -25,7 +25,10 @@ from awx.main.models.base import * # noqa from awx.main.models.jobs import Job from awx.main.models.unified_jobs import * # noqa from awx.main.models.mixins import ResourceMixin -from awx.main.models.notifications import NotificationTemplate +from awx.main.models.notifications import ( + NotificationTemplate, + JobNotificationMixin, +) from awx.main.utils import _inventory_updates from awx.main.conf import tower_settings @@ -1192,7 +1195,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions): return source -class InventoryUpdate(UnifiedJob, InventorySourceOptions): +class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin): ''' Internal job for tracking inventory updates from external sources. ''' @@ -1268,6 +1271,15 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions): return False return True + ''' + JobNotificationMixin + ''' + def get_notification_templates(self): + return self.inventory_source.notification_templates + + def get_notification_friendly_name(self): + return "Inventory Update" + class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin): diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 4709ab43c6..8d47ed7938 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -24,7 +24,10 @@ from jsonfield import JSONField from awx.main.constants import CLOUD_PROVIDERS from awx.main.models.base import * # noqa from awx.main.models.unified_jobs import * # noqa -from awx.main.models.notifications import NotificationTemplate +from awx.main.models.notifications import ( + NotificationTemplate, + JobNotificationMixin, +) from awx.main.utils import decrypt_field, ignore_inventory_computed_fields from awx.main.utils import emit_websocket_notification from awx.main.redact import PlainTextCleaner @@ -499,7 +502,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): any_notification_templates = set(any_notification_templates + list(base_notification_templates.filter(organization_notification_templates_for_any=self.project.organization))) return dict(error=list(error_notification_templates), success=list(success_notification_templates), any=list(any_notification_templates)) -class Job(UnifiedJob, JobOptions): +class Job(UnifiedJob, JobOptions, JobNotificationMixin): ''' A job applies a project (with playbook) to an inventory source with a given credential. It represents a single invocation of ansible-playbook with the @@ -792,6 +795,15 @@ class Job(UnifiedJob, JobOptions): return True + ''' + JobNotificationMixin + ''' + def get_notification_templates(self): + return self.job_template.notification_templates + + def get_notification_friendly_name(self): + return "Job" + class JobHostSummary(CreatedModifiedModel): ''' Per-host statistics for each job. @@ -1315,7 +1327,7 @@ class SystemJobTemplate(UnifiedJobTemplate, SystemJobOptions): any=list(any_notification_templates)) -class SystemJob(UnifiedJob, SystemJobOptions): +class SystemJob(UnifiedJob, SystemJobOptions, JobNotificationMixin): class Meta: app_label = 'main' @@ -1378,3 +1390,13 @@ class SystemJob(UnifiedJob, SystemJobOptions): @property def task_impact(self): return 150 + + ''' + JobNotificationMixin + ''' + def get_notification_templates(self): + return self.system_job_template.notification_templates + + def get_notification_friendly_name(self): + return "System Job" + diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index 75fffea642..442b5dc2c8 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -171,3 +171,27 @@ class Notification(CreatedModifiedModel): def get_absolute_url(self): return reverse('api:notification_detail', args=(self.pk,)) + +class JobNotificationMixin(object): + def get_notification_templates(self): + raise RuntimeError("Define me") + + def get_notification_friendly_name(self): + raise RuntimeError("Define me") + + def _build_notification_message(self, status_str): + notification_body = self.notification_data() + notification_subject = "{} #{} '{}' {} on Ansible Tower: {}".format(self.get_notification_friendly_name(), + self.id, + self.name, + status_str, + notification_body['url']) + notification_body['friendly_name'] = self.get_notification_friendly_name() + return (notification_subject, notification_body) + + def build_notification_succeeded_message(self): + return self._build_notification_message('succeeded') + + def build_notification_failed_message(self): + return self._build_notification_message('failed') + diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 93c4a42e36..85ca3ab2aa 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -20,7 +20,10 @@ from django.utils.timezone import now, make_aware, get_default_timezone from awx.lib.compat import slugify from awx.main.models.base import * # noqa from awx.main.models.jobs import Job -from awx.main.models.notifications import NotificationTemplate +from awx.main.models.notifications import ( + NotificationTemplate, + JobNotificationMixin, +) from awx.main.models.unified_jobs import * # noqa from awx.main.models.mixins import ResourceMixin from awx.main.utils import update_scm_url @@ -372,8 +375,7 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): def get_absolute_url(self): return reverse('api:project_detail', args=(self.pk,)) - -class ProjectUpdate(UnifiedJob, ProjectOptions): +class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin): ''' Internal job for tracking project updates from SCM. ''' @@ -443,3 +445,12 @@ class ProjectUpdate(UnifiedJob, ProjectOptions): if 'scm_delete_on_next_update' not in update_fields: update_fields.append('scm_delete_on_next_update') parent_instance.save(update_fields=update_fields) + + ''' + JobNotificationMixin + ''' + def get_notification_templates(self): + return self.project.notification_templates + + def get_notification_friendly_name(self): + return "Project Update" diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index f39ee35c4c..950b6fc99b 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -18,6 +18,7 @@ from django.core.exceptions import NON_FIELD_ERRORS from django.utils.translation import ugettext_lazy as _ from django.utils.timezone import now from django.utils.encoding import smart_text +from django.apps import apps # Django-JSONField from jsonfield import JSONField @@ -360,8 +361,30 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio dest_field.add(*list(src_field_value.all().values_list('id', flat=True))) return unified_job +class UnifiedJobTypeStringMixin(object): + @classmethod + def _underscore_to_camel(cls, word): + return ''.join(x.capitalize() or '_' for x in word.split('_')) -class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique): + @classmethod + def _model_type(cls, job_type): + # Django >= 1.9 + #app = apps.get_app_config('main') + model_str = cls._underscore_to_camel(job_type) + try: + return apps.get_model('main', model_str) + except LookupError: + print("Lookup model error") + return None + + @classmethod + def get_instance_by_type(cls, job_type, job_id): + model = cls._model_type(job_type) + if not model: + return None + return model.objects.get(id=job_id) + +class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique, UnifiedJobTypeStringMixin): ''' Concrete base class for unified job run by the task engine. ''' diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 55989b1514..b77275c0fd 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -185,114 +185,61 @@ def notify_task_runner(metadata_dict): queue = FifoQueue('tower_task_manager') queue.push(metadata_dict) + +def _send_notification_templates(instance, status_str): + if status_str not in ['succeeded', 'failed']: + raise ValueError("status_str must be either succeeded or failed") + print("Instance has some shit in it %s" % instance) + notification_templates = instance.get_notification_templates() + if notification_templates: + all_notification_templates = set(notification_templates.get('success', []) + notification_templates.get('any', [])) + if len(all_notification_templates): + try: + (notification_subject, notification_body) = getattr(instance, 'build_notification_%s_message' % status_str)() + except AttributeError: + raise NotImplementedError("build_notification_%s_message() does not exist" % status_str) + send_notifications.delay([n.generate_notification(notification_subject, notification_body).id + for n in all_notification_templates], + job_id=instance.id) + @task(bind=True) def handle_work_success(self, result, task_actual): - if task_actual['type'] == 'project_update': - instance = ProjectUpdate.objects.get(id=task_actual['id']) - instance_name = instance.name - notification_templates = instance.project.notification_templates - friendly_name = "Project Update" - elif task_actual['type'] == 'inventory_update': - instance = InventoryUpdate.objects.get(id=task_actual['id']) - instance_name = instance.name - notification_templates = instance.inventory_source.notification_templates - friendly_name = "Inventory Update" - elif task_actual['type'] == 'job': - instance = Job.objects.get(id=task_actual['id']) - instance_name = instance.job_template.name - notification_templates = instance.job_template.notification_templates - friendly_name = "Job" - elif task_actual['type'] == 'ad_hoc_command': - instance = AdHocCommand.objects.get(id=task_actual['id']) - instance_name = instance.module_name - notification_templates = instance.notification_templates - friendly_name = "AdHoc Command" - elif task_actual['type'] == 'system_job': - instance = SystemJob.objects.get(id=task_actual['id']) - instance_name = instance.system_job_template.name - notification_templates = instance.system_job_template.notification_templates - friendly_name = "System Job" - else: + instance = UnifiedJob.get_instance_by_type(task_actual['type'], task_actual['id']) + if not instance: return - all_notification_templates = set(notification_templates.get('success', []) + notification_templates.get('any', [])) - if len(all_notification_templates): - notification_body = instance.notification_data() - notification_subject = "{} #{} '{}' succeeded on Ansible Tower: {}".format(friendly_name, - task_actual['id'], - smart_str(instance_name), - notification_body['url']) - notification_body['friendly_name'] = friendly_name - send_notifications.delay([n.generate_notification(notification_subject, notification_body).id - for n in all_notification_templates], - job_id=task_actual['id']) + _send_notification_templates(instance, 'succeeded') @task(bind=True) def handle_work_error(self, task_id, subtasks=None): print('Executing error task id %s, subtasks: %s' % (str(self.request.id), str(subtasks))) - first_task = None - first_task_id = None - first_task_type = '' - first_task_name = '' + first_instance = None + first_instance_type = '' if subtasks is not None: for each_task in subtasks: - instance_name = '' - if each_task['type'] == 'project_update': - instance = ProjectUpdate.objects.get(id=each_task['id']) - instance_name = instance.name - notification_templates = instance.project.notification_templates - friendly_name = "Project Update" - elif each_task['type'] == 'inventory_update': - instance = InventoryUpdate.objects.get(id=each_task['id']) - instance_name = instance.name - notification_templates = instance.inventory_source.notification_templates - friendly_name = "Inventory Update" - elif each_task['type'] == 'job': - instance = Job.objects.get(id=each_task['id']) - instance_name = instance.job_template.name - notification_templates = instance.job_template.notification_templates - friendly_name = "Job" - elif each_task['type'] == 'ad_hoc_command': - instance = AdHocCommand.objects.get(id=each_task['id']) - instance_name = instance.module_name - notification_templates = instance.notification_templates - friendly_name = "AdHoc Command" - elif each_task['type'] == 'system_job': - instance = SystemJob.objects.get(id=each_task['id']) - instance_name = instance.system_job_template.name - notification_templates = instance.system_job_template.notification_templates - friendly_name = "System Job" - else: + instance = UnifiedJob.get_instance_by_type(each_task['type'], each_task['id']) + if not instance: # Unknown task type logger.warn("Unknown task type: {}".format(each_task['type'])) continue - if first_task is None: - first_task = instance - first_task_id = instance.id - first_task_type = each_task['type'] - first_task_name = instance_name - first_task_friendly_name = friendly_name + + if first_instance is None: + first_instance = instance + first_instance_type = each_task['type'] + if instance.celery_task_id != task_id: instance.status = 'failed' instance.failed = True instance.job_explanation = 'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % \ - (first_task_type, first_task_name, first_task_id) + (first_instance_type, first_instance.name, first_instance.id) instance.save() instance.socketio_emit_status("failed") - all_notification_templates = set(notification_templates.get('error', []) + notification_templates.get('any', [])) - if len(all_notification_templates): - notification_body = first_task.notification_data() - notification_subject = "{} #{} '{}' failed on Ansible Tower: {}".format(first_task_friendly_name, - first_task_id, - smart_str(first_task_name), - notification_body['url']) - notification_body['friendly_name'] = first_task_friendly_name - send_notifications.delay([n.generate_notification(notification_subject, notification_body).id - for n in all_notification_templates], - job_id=first_task_id) - + if first_instance: + print("Instance type is %s" % first_instance_type) + print("Instance passing along %s" % first_instance.name) + _send_notification_templates(first_instance, 'failed') @task() def update_inventory_computed_fields(inventory_id, should_update_hosts=True): @@ -1710,3 +1657,4 @@ class RunSystemJob(BaseTask): def build_cwd(self, instance, **kwargs): return settings.BASE_DIR +