diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index edf03a883d..c95c8488bd 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1185,11 +1185,10 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions): @property def notifiers(self): - # Return all notifiers defined on the Project, and on the Organization for each trigger type base_notifiers = Notifier.objects.filter(active=True) - error_notifiers = list(base_notifiers.filter(organization_notifiers_for_errors__in=[self])) - success_notifiers = list(base_notifiers.filter(organization_notifiers_for_success__in=[self])) - any_notifiers = list(base_notifiers.filter(organization_notifiers_for_any__in=[self])) + error_notifiers = list(base_notifiers.filter(organization_notifiers_for_errors=self.inventory.organization)) + success_notifiers = list(base_notifiers.filter(organization_notifiers_for_success=self.inventory.organization)) + any_notifiers = list(base_notifiers.filter(organization_notifiers_for_any=self.inventory.organization)) return dict(error=error_notifiers, success=success_notifiers, any=any_notifiers) def clean_source(self): diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 01857b8b06..bd167d3474 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -341,7 +341,11 @@ class JobTemplate(UnifiedJobTemplate, JobOptions): error_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_errors__in=[self, self.project])) success_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_success__in=[self, self.project])) any_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_any__in=[self, self.project])) - return dict(error=error_notifiers, success=success_notifiers, any=any_notifiers) + # Get Organization Notifiers + error_notifiers = set(error_notifiers + list(base_notifiers.filter(organization_notifiers_for_errors__in=self.project.organizations.all()))) + success_notifiers = set(success_notifiers + list(base_notifiers.filter(organization_notifiers_for_success__in=self.project.organizations.all()))) + any_notifiers = set(any_notifiers + list(base_notifiers.filter(organization_notifiers_for_any__in=self.project.organizations.all()))) + return dict(error=list(error_notifiers), success=list(success_notifiers), any=list(any_notifiers)) class Job(UnifiedJob, JobOptions): ''' diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 415c674bb1..db295023da 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -313,20 +313,15 @@ class Project(UnifiedJobTemplate, ProjectOptions): @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 = Notifier.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_notifiers_for_errors=self)) success_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_success=self)) any_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_any=self)) - return dict(error=error_notifiers, success=success_notifiers, any=any_notifiers) + # Get Organization Notifiers + error_notifiers = set(error_notifiers + list(base_notifiers.filter(organization_notifiers_for_errors__in=self.organizations.all()))) + success_notifiers = set(success_notifiers + list(base_notifiers.filter(organization_notifiers_for_success__in=self.organizations.all()))) + any_notifiers = set(any_notifiers + list(base_notifiers.filter(organization_notifiers_for_any__in=self.organizations.all()))) + return dict(error=list(error_notifiers), success=list(success_notifiers), any=list(any_notifiers)) def get_absolute_url(self): return reverse('api:project_detail', args=(self.pk,)) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 7bb4cdd798..d83cfae978 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -17,6 +17,7 @@ from django.db import models 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 # Django-JSONField from jsonfield import JSONField @@ -741,7 +742,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique return dict(id=self.id, name=self.name, url=self.get_ui_url(), - created_by=str(self.created_by), + created_by=smart_text(self.created_by), started=self.started.isoformat(), finished=self.finished.isoformat(), status=self.status, diff --git a/awx/main/notifications/twilio_backend.py b/awx/main/notifications/twilio_backend.py index 1aea6f368e..df411c68c5 100644 --- a/awx/main/notifications/twilio_backend.py +++ b/awx/main/notifications/twilio_backend.py @@ -20,11 +20,10 @@ class TwilioBackend(TowerBaseEmailBackend): 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, fail_silently=False, **kwargs): super(TwilioBackend, self).__init__(fail_silently=fail_silently) self.account_sid = account_sid self.account_token = account_token - self.from_phone = from_phone def send_messages(self, messages): sent_messages = 0 diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 4b285546bb..509c5d1e7e 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -235,7 +235,7 @@ def handle_work_success(self, result, task_actual): instance_name, notification_body['url']) send_notifications.delay([n.generate_notification(notification_subject, notification_body) - for n in notifiers.get('success', []) + notifiers.get('any', [])], + for n in set(notifiers.get('success', []) + notifiers.get('any', []))], job_id=task_actual['id']) @task(bind=True) @@ -292,7 +292,7 @@ def handle_work_error(self, task_id, subtasks=None): 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 notifiers.get('error', []) + notifiers.get('any', [])], + for n in set(notifiers.get('error', []) + notifiers.get('any', []))], job_id=first_task_id) diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py new file mode 100644 index 0000000000..27ff76f816 --- /dev/null +++ b/awx/main/tests/functional/conftest.py @@ -0,0 +1,160 @@ +import pytest +import mock + +from django.core.urlresolvers import resolve +from django.utils.six.moves.urllib.parse import urlparse + +from awx.main.models.organization import Organization +from awx.main.models.projects import Project +from awx.main.models.ha import Instance +from django.contrib.auth.models import User +from rest_framework.test import ( + APIRequestFactory, + force_authenticate, +) + +@pytest.fixture +def user(): + def u(name, is_superuser=False): + try: + user = User.objects.get(username=name) + except User.DoesNotExist: + user = User(username=name, is_superuser=is_superuser, password=name) + user.save() + return user + return u + +@pytest.fixture +def post(): + def rf(url, data, user=None, middleware=None, **kwargs): + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + if 'format' not in kwargs: + kwargs['format'] = 'json' + request = APIRequestFactory().post(url, data, **kwargs) + if middleware: + middleware.process_request(request) + if user: + force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) + if middleware: + middleware.process_response(request, response) + return response + return rf + +@pytest.fixture +def get(): + def rf(url, user=None, middleware=None, **kwargs): + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + if 'format' not in kwargs: + kwargs['format'] = 'json' + request = APIRequestFactory().get(url, **kwargs) + if middleware: + middleware.process_request(request) + if user: + force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) + if middleware: + middleware.process_response(request, response) + return response + return rf + +@pytest.fixture +def put(): + def rf(url, data, user=None, middleware=None, **kwargs): + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + if 'format' not in kwargs: + kwargs['format'] = 'json' + request = APIRequestFactory().put(url, data, **kwargs) + if middleware: + middleware.process_request(request) + if user: + force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) + if middleware: + middleware.process_response(request, response) + return response + return rf + +@pytest.fixture +def patch(): + def rf(url, data, user=None, middleware=None, **kwargs): + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + if 'format' not in kwargs: + kwargs['format'] = 'json' + request = APIRequestFactory().patch(url, data, **kwargs) + if middleware: + middleware.process_request(request) + if user: + force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) + if middleware: + middleware.process_response(request, response) + return response + return rf + +@pytest.fixture +def delete(): + def rf(url, user=None, middleware=None, **kwargs): + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + if 'format' not in kwargs: + kwargs['format'] = 'json' + request = APIRequestFactory().delete(url, **kwargs) + if middleware: + middleware.process_request(request) + if user: + force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) + if middleware: + middleware.process_response(request, response) + return response + return rf + +@pytest.fixture +def head(): + def rf(url, user=None, middleware=None, **kwargs): + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + if 'format' not in kwargs: + kwargs['format'] = 'json' + request = APIRequestFactory().head(url, **kwargs) + if middleware: + middleware.process_request(request) + if user: + force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) + if middleware: + middleware.process_response(request, response) + return response + return rf + +@pytest.fixture +def options(): + def rf(url, data, user=None, middleware=None, **kwargs): + view, view_args, view_kwargs = resolve(urlparse(url)[2]) + if 'format' not in kwargs: + kwargs['format'] = 'json' + request = APIRequestFactory().options(url, data, **kwargs) + if middleware: + middleware.process_request(request) + if user: + force_authenticate(request, user=user) + response = view(request, *view_args, **view_kwargs) + if middleware: + middleware.process_response(request, response) + return response + return rf + +@pytest.fixture +def instance(settings): + return Instance.objects.create(uuid=settings.SYSTEM_UUID, primary=True, hostname="instance.example.org") + +@pytest.fixture +def organization(instance): + return Organization.objects.create(name="test-org", description="test-org-desc") + +@pytest.fixture +@mock.patch.object(Project, "update", lambda self, **kwargs: None) +def project(instance): + return Project.objects.create(name="test-proj", + description="test-proj-desc", + scm_type="git", + scm_url="https://github.com/jlaska/ansible-playbooks") diff --git a/awx/main/tests/functional/test_notifications.py b/awx/main/tests/functional/test_notifications.py new file mode 100644 index 0000000000..72ac91b7b1 --- /dev/null +++ b/awx/main/tests/functional/test_notifications.py @@ -0,0 +1,124 @@ +import mock +import pytest + +from awx.main.models.notifications import Notification, Notifier +from awx.main.models.inventory import Inventory, Group +from awx.main.models.organization import Organization +from awx.main.models.projects import Project +from awx.main.models.jobs import JobTemplate + +from django.core.urlresolvers import reverse +from django.core.mail.message import EmailMessage + +@pytest.fixture +def notifier(): + return Notifier.objects.create(name="test-notification", + notification_type="webhook", + notification_configuration=dict(url="http://localhost", + headers={"Test": "Header"})) + +@pytest.mark.django_db +def test_get_notifier_list(get, user, notifier): + url = reverse('api:notifier_list') + response = get(url, user('admin', True)) + assert response.status_code == 200 + assert len(response.data['results']) == 1 + +@pytest.mark.django_db +def test_basic_parameterization(get, post, user, organization): + u = user('admin-poster', True) + url = reverse('api:notifier_list') + response = post(url, + dict(name="test-webhook", + description="test webhook", + organization=1, + notification_type="webhook", + notification_configuration=dict(url="http://localhost", + headers={"Test": "Header"})), + u) + assert response.status_code == 201 + url = reverse('api:notifier_detail', args=(response.data['id'],)) + response = get(url, u) + assert 'related' in response.data + assert 'organization' in response.data['related'] + assert 'summary_fields' in response.data + assert 'organization' in response.data['summary_fields'] + assert 'notifications' in response.data['related'] + assert 'notification_configuration' in response.data + assert 'url' in response.data['notification_configuration'] + assert 'headers' in response.data['notification_configuration'] + +@pytest.mark.django_db +def test_encrypted_subfields(get, post, user, organization): + def assert_send(self, messages): + assert self.account_token == "shouldhide" + return 1 + u = user('admin-poster', True) + url = reverse('api:notifier_list') + response = post(url, + dict(name="test-twilio", + description="test twilio", + organization=1, + notification_type="twilio", + notification_configuration=dict(account_sid="dummy", + account_token="shouldhide", + from_number="+19999999999", + to_numbers=["9998887777"])), + u) + assert response.status_code == 201 + notifier_actual = Notifier.objects.get(id=response.data['id']) + assert notifier_actual.notification_configuration['account_token'].startswith("$encrypted$") + url = reverse('api:notifier_detail', args=(response.data['id'],)) + response = get(url, u) + assert response.data['notification_configuration']['account_token'] == "$encrypted$" + with mock.patch.object(notifier_actual.notification_class, "send_messages", assert_send): + notifier_actual.send("Test", {'body': "Test"}) + +@pytest.mark.django_db +def test_inherited_notifiers(get, post, user, organization, project): + u = user('admin-poster', True) + url = reverse('api:notifier_list') + notifiers = [] + for nfiers in xrange(3): + response = post(url, + dict(name="test-webhook-{}".format(nfiers), + description="test webhook {}".format(nfiers), + organization=1, + notification_type="webhook", + notification_configuration=dict(url="http://localhost", + headers={"Test": "Header"})), + u) + assert response.status_code == 201 + notifiers.append(response.data['id']) + o = Organization.objects.get(id=1) + p = Project.objects.get(id=1) + o.projects.add(p) + i = Inventory.objects.create(name='test', organization=o) + i.save() + g = Group.objects.create(name='test', inventory=i) + g.save() + jt = JobTemplate.objects.create(name='test', inventory=i, project=p, playbook='debug.yml') + jt.save() + url = reverse('api:organization_notifiers_any_list', args=(1,)) + response = post(url, dict(id=notifiers[0]), u) + assert response.status_code == 204 + url = reverse('api:project_notifiers_any_list', args=(1,)) + response = post(url, dict(id=notifiers[1]), u) + assert response.status_code == 204 + url = reverse('api:job_template_notifiers_any_list', args=(jt.id,)) + response = post(url, dict(id=notifiers[2]), u) + assert response.status_code == 204 + assert len(jt.notifiers['any']) == 3 + assert len(p.notifiers['any']) == 2 + assert len(g.inventory_source.notifiers['any']) == 1 + +@pytest.mark.django_db +def test_notifier_merging(get, post, user, organization, project, notifier): + u = user('admin-poster', True) + o = Organization.objects.get(id=1) + p = Project.objects.get(id=1) + n = Notifier.objects.get(id=1) + o.projects.add(p) + o.notifiers_any.add(n) + p.notifiers_any.add(n) + assert len(p.notifiers['any']) == 1