mirror of
https://github.com/ansible/awx.git
synced 2026-05-19 14:57:39 -02:30
Merge pull request #4124 from beeankha/webhook_enhancement
Webhook Custom HTTP Method + Basic Auth Support Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
@@ -4237,6 +4237,8 @@ class NotificationTemplateSerializer(BaseSerializer):
|
|||||||
continue
|
continue
|
||||||
if field_type == "password" and field_val == "$encrypted$" and object_actual is not None:
|
if field_type == "password" and field_val == "$encrypted$" and object_actual is not None:
|
||||||
attrs['notification_configuration'][field] = object_actual.notification_configuration[field]
|
attrs['notification_configuration'][field] = object_actual.notification_configuration[field]
|
||||||
|
if field == "http_method" and field_val.lower() not in ['put', 'post']:
|
||||||
|
error_list.append(_("HTTP method must be either 'POST' or 'PUT'."))
|
||||||
if missing_fields:
|
if missing_fields:
|
||||||
error_list.append(_("Missing required fields for Notification Configuration: {}.").format(missing_fields))
|
error_list.append(_("Missing required fields for Notification Configuration: {}.").format(missing_fields))
|
||||||
if incorrect_type_fields:
|
if incorrect_type_fields:
|
||||||
|
|||||||
25
awx/main/migrations/0082_v360_webhook_http_method.py
Normal file
25
awx/main/migrations/0082_v360_webhook_http_method.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def add_webhook_notification_template_fields(apps, schema_editor):
|
||||||
|
# loop over all existing webhook notification templates and make
|
||||||
|
# sure they have the new "http_method" field filled in with "POST"
|
||||||
|
NotificationTemplate = apps.get_model('main', 'notificationtemplate')
|
||||||
|
webhooks = NotificationTemplate.objects.filter(notification_type='webhook')
|
||||||
|
for w in webhooks:
|
||||||
|
w.notification_configuration['http_method'] = 'POST'
|
||||||
|
w.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0081_v360_notify_on_start'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(add_webhook_notification_template_fields, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -118,9 +118,10 @@ class NotificationTemplate(CommonModelNameNotUnique):
|
|||||||
def send(self, subject, body):
|
def send(self, subject, body):
|
||||||
for field in filter(lambda x: self.notification_class.init_parameters[x]['type'] == "password",
|
for field in filter(lambda x: self.notification_class.init_parameters[x]['type'] == "password",
|
||||||
self.notification_class.init_parameters):
|
self.notification_class.init_parameters):
|
||||||
self.notification_configuration[field] = decrypt_field(self,
|
if field in self.notification_configuration:
|
||||||
'notification_configuration',
|
self.notification_configuration[field] = decrypt_field(self,
|
||||||
subfield=field)
|
'notification_configuration',
|
||||||
|
subfield=field)
|
||||||
recipients = self.notification_configuration.pop(self.notification_class.recipient_parameter)
|
recipients = self.notification_configuration.pop(self.notification_class.recipient_parameter)
|
||||||
if not isinstance(recipients, list):
|
if not isinstance(recipients, list):
|
||||||
recipients = [recipients]
|
recipients = [recipients]
|
||||||
|
|||||||
@@ -15,14 +15,20 @@ logger = logging.getLogger('awx.main.notifications.webhook_backend')
|
|||||||
class WebhookBackend(AWXBaseEmailBackend):
|
class WebhookBackend(AWXBaseEmailBackend):
|
||||||
|
|
||||||
init_parameters = {"url": {"label": "Target URL", "type": "string"},
|
init_parameters = {"url": {"label": "Target URL", "type": "string"},
|
||||||
|
"http_method": {"label": "HTTP Method", "type": "string", "default": "POST"},
|
||||||
"disable_ssl_verification": {"label": "Verify SSL", "type": "bool", "default": False},
|
"disable_ssl_verification": {"label": "Verify SSL", "type": "bool", "default": False},
|
||||||
|
"username": {"label": "Username", "type": "string", "default": ""},
|
||||||
|
"password": {"label": "Password", "type": "password", "default": ""},
|
||||||
"headers": {"label": "HTTP Headers", "type": "object"}}
|
"headers": {"label": "HTTP Headers", "type": "object"}}
|
||||||
recipient_parameter = "url"
|
recipient_parameter = "url"
|
||||||
sender_parameter = None
|
sender_parameter = None
|
||||||
|
|
||||||
def __init__(self, headers, disable_ssl_verification=False, fail_silently=False, **kwargs):
|
def __init__(self, http_method, headers, disable_ssl_verification=False, fail_silently=False, username=None, password=None, **kwargs):
|
||||||
|
self.http_method = http_method
|
||||||
self.disable_ssl_verification = disable_ssl_verification
|
self.disable_ssl_verification = disable_ssl_verification
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
super(WebhookBackend, self).__init__(fail_silently=fail_silently)
|
super(WebhookBackend, self).__init__(fail_silently=fail_silently)
|
||||||
|
|
||||||
def format_body(self, body):
|
def format_body(self, body):
|
||||||
@@ -32,8 +38,15 @@ class WebhookBackend(AWXBaseEmailBackend):
|
|||||||
sent_messages = 0
|
sent_messages = 0
|
||||||
if 'User-Agent' not in self.headers:
|
if 'User-Agent' not in self.headers:
|
||||||
self.headers['User-Agent'] = "Tower {}".format(get_awx_version())
|
self.headers['User-Agent'] = "Tower {}".format(get_awx_version())
|
||||||
|
if self.http_method.lower() not in ['put','post']:
|
||||||
|
raise ValueError("HTTP method must be either 'POST' or 'PUT'.")
|
||||||
|
chosen_method = getattr(requests, self.http_method.lower(), None)
|
||||||
for m in messages:
|
for m in messages:
|
||||||
r = requests.post("{}".format(m.recipients()[0]),
|
auth = None
|
||||||
|
if self.username or self.password:
|
||||||
|
auth = (self.username, self.password)
|
||||||
|
r = chosen_method("{}".format(m.recipients()[0]),
|
||||||
|
auth=auth,
|
||||||
json=m.body,
|
json=m.body,
|
||||||
headers=self.headers,
|
headers=self.headers,
|
||||||
verify=(not self.disable_ssl_verification))
|
verify=(not self.disable_ssl_verification))
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ def mk_credential(name, credential_type='ssh', persisted=True):
|
|||||||
def mk_notification_template(name, notification_type='webhook', configuration=None, organization=None, persisted=True):
|
def mk_notification_template(name, notification_type='webhook', configuration=None, organization=None, persisted=True):
|
||||||
nt = NotificationTemplate(name=name)
|
nt = NotificationTemplate(name=name)
|
||||||
nt.notification_type = notification_type
|
nt.notification_type = notification_type
|
||||||
nt.notification_configuration = configuration or dict(url="http://localhost", headers={"Test": "Header"})
|
nt.notification_configuration = configuration or dict(url="http://localhost", username="", password="", headers={"Test": "Header"})
|
||||||
|
|
||||||
if organization is not None:
|
if organization is not None:
|
||||||
nt.organization = organization
|
nt.organization = organization
|
||||||
@@ -216,7 +216,7 @@ def mk_workflow_job_template(name, extra_vars='', spec=None, organization=None,
|
|||||||
|
|
||||||
|
|
||||||
def mk_workflow_job_template_node(workflow_job_template=None,
|
def mk_workflow_job_template_node(workflow_job_template=None,
|
||||||
unified_job_template=None,
|
unified_job_template=None,
|
||||||
success_nodes=None,
|
success_nodes=None,
|
||||||
failure_nodes=None,
|
failure_nodes=None,
|
||||||
always_nodes=None,
|
always_nodes=None,
|
||||||
@@ -231,11 +231,11 @@ def mk_workflow_job_template_node(workflow_job_template=None,
|
|||||||
return workflow_node
|
return workflow_node
|
||||||
|
|
||||||
|
|
||||||
def mk_workflow_job_node(unified_job_template=None,
|
def mk_workflow_job_node(unified_job_template=None,
|
||||||
success_nodes=None,
|
success_nodes=None,
|
||||||
failure_nodes=None,
|
failure_nodes=None,
|
||||||
always_nodes=None,
|
always_nodes=None,
|
||||||
workflow_job=None,
|
workflow_job=None,
|
||||||
job=None,
|
job=None,
|
||||||
persisted=True):
|
persisted=True):
|
||||||
workflow_node = WorkflowJobNode(unified_job_template=unified_job_template,
|
workflow_node = WorkflowJobNode(unified_job_template=unified_job_template,
|
||||||
@@ -247,4 +247,3 @@ def mk_workflow_job_node(unified_job_template=None,
|
|||||||
if persisted:
|
if persisted:
|
||||||
workflow_node.save()
|
workflow_node.save()
|
||||||
return workflow_node
|
return workflow_node
|
||||||
|
|
||||||
|
|||||||
@@ -385,7 +385,9 @@ def notification_template(organization):
|
|||||||
organization=organization,
|
organization=organization,
|
||||||
notification_type="webhook",
|
notification_type="webhook",
|
||||||
notification_configuration=dict(url="http://localhost",
|
notification_configuration=dict(url="http://localhost",
|
||||||
headers={"Test": "Header"}))
|
username="",
|
||||||
|
password="",
|
||||||
|
headers={"Test": "Header",}))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ def test_inherited_notification_templates(get, post, user, organization, project
|
|||||||
isrc.save()
|
isrc.save()
|
||||||
jt = JobTemplate.objects.create(name='test', inventory=i, project=project, playbook='debug.yml')
|
jt = JobTemplate.objects.create(name='test', inventory=i, project=project, playbook='debug.yml')
|
||||||
jt.save()
|
jt.save()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_notification_template_simple_patch(patch, notification_template, admin):
|
def test_notification_template_simple_patch(patch, notification_template, admin):
|
||||||
@@ -124,7 +124,7 @@ def test_custom_environment_injection(post, user, organization):
|
|||||||
organization=organization.id,
|
organization=organization.id,
|
||||||
notification_type="webhook",
|
notification_type="webhook",
|
||||||
notification_configuration=dict(url="https://example.org", disable_ssl_verification=False,
|
notification_configuration=dict(url="https://example.org", disable_ssl_verification=False,
|
||||||
headers={"Test": "Header"})),
|
http_method="POST", headers={"Test": "Header"})),
|
||||||
u)
|
u)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
template = NotificationTemplate.objects.get(pk=response.data['id'])
|
template = NotificationTemplate.objects.get(pk=response.data['id'])
|
||||||
|
|||||||
@@ -87,6 +87,15 @@ export default ['Rest', 'Wait', 'NotificationsFormObject',
|
|||||||
element: '#notification_template_color',
|
element: '#notification_template_color',
|
||||||
multiple: false
|
multiple: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$scope.httpMethodChoices = [
|
||||||
|
{'id': 'POST', 'name': i18n._('POST')},
|
||||||
|
{'id': 'PUT', 'name': i18n._('PUT')},
|
||||||
|
];
|
||||||
|
CreateSelect2({
|
||||||
|
element: '#notification_template_http_method',
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.$watch('headers', function validate_headers(str) {
|
$scope.$watch('headers', function validate_headers(str) {
|
||||||
|
|||||||
@@ -138,6 +138,16 @@ export default ['Rest', 'Wait',
|
|||||||
element: '#notification_template_color',
|
element: '#notification_template_color',
|
||||||
multiple: false
|
multiple: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$scope.httpMethodChoices = [
|
||||||
|
{'id': 'POST', 'name': i18n._('POST')},
|
||||||
|
{'id': 'PUT', 'name': i18n._('PUT')},
|
||||||
|
];
|
||||||
|
CreateSelect2({
|
||||||
|
element: '#notification_template_http_method',
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
|
||||||
NotificationsTypeChange.getDetailFields($scope.notification_type.value).forEach(function(field) {
|
NotificationsTypeChange.getDetailFields($scope.notification_type.value).forEach(function(field) {
|
||||||
$scope[field[0]] = field[1];
|
$scope[field[0]] = field[1];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,8 +68,12 @@ angular.module('notifications', [
|
|||||||
var url = getBasePath('notification_templates') + notificationTemplateId + '/';
|
var url = getBasePath('notification_templates') + notificationTemplateId + '/';
|
||||||
rest.setUrl(url);
|
rest.setUrl(url);
|
||||||
return rest.get()
|
return rest.get()
|
||||||
.then(function(data) {
|
.then(function(res) {
|
||||||
return data.data;
|
if (_.get(res, ['data', 'notification_type'] === 'webhook') &&
|
||||||
|
_.get(res, ['data', 'notification_configuration', 'http_method'])) {
|
||||||
|
res.data.notification_configuration.http_method = res.data.notification_configuration.http_method.toUpperCase();
|
||||||
|
}
|
||||||
|
return res.data;
|
||||||
}).catch(function(response) {
|
}).catch(function(response) {
|
||||||
ProcessErrors(null, response.data, response.status, null, {
|
ProcessErrors(null, response.data, response.status, null, {
|
||||||
hdr: 'Error!',
|
hdr: 'Error!',
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default ['i18n', function(i18n) {
|
|||||||
username: {
|
username: {
|
||||||
label: i18n._('Username'),
|
label: i18n._('Username'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
ngShow: "notification_type.value == 'email' ",
|
ngShow: "notification_type.value == 'email' || notification_type.value == 'webhook' ",
|
||||||
subForm: 'typeSubForm',
|
subForm: 'typeSubForm',
|
||||||
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
|
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
|
||||||
},
|
},
|
||||||
@@ -75,7 +75,7 @@ export default ['i18n', function(i18n) {
|
|||||||
reqExpression: "password_required" ,
|
reqExpression: "password_required" ,
|
||||||
init: "false"
|
init: "false"
|
||||||
},
|
},
|
||||||
ngShow: "notification_type.value == 'email' || notification_type.value == 'irc' ",
|
ngShow: "notification_type.value == 'email' || notification_type.value == 'irc' || notification_type.value == 'webhook' ",
|
||||||
subForm: 'typeSubForm',
|
subForm: 'typeSubForm',
|
||||||
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
|
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
|
||||||
},
|
},
|
||||||
@@ -423,6 +423,21 @@ export default ['i18n', function(i18n) {
|
|||||||
subForm: 'typeSubForm',
|
subForm: 'typeSubForm',
|
||||||
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
|
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
|
||||||
},
|
},
|
||||||
|
http_method: {
|
||||||
|
label: i18n._('HTTP Method'),
|
||||||
|
dataTitle: i18n._('HTTP Method'),
|
||||||
|
type: 'select',
|
||||||
|
ngOptions: 'choice.id as choice.name for choice in httpMethodChoices',
|
||||||
|
default: 'post',
|
||||||
|
awPopOver: i18n._('Specify an HTTP method for the webhook. Acceptable choices are: POST or PUT'),
|
||||||
|
awRequiredWhen: {
|
||||||
|
reqExpression: "webhook_required",
|
||||||
|
init: "false"
|
||||||
|
},
|
||||||
|
ngShow: "notification_type.value == 'webhook' ",
|
||||||
|
subForm: 'typeSubForm',
|
||||||
|
ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)'
|
||||||
|
},
|
||||||
mattermost_url: {
|
mattermost_url: {
|
||||||
label: i18n._('Target URL'),
|
label: i18n._('Target URL'),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ function (i18n) {
|
|||||||
break;
|
break;
|
||||||
case 'webhook':
|
case 'webhook':
|
||||||
obj.webhook_required = true;
|
obj.webhook_required = true;
|
||||||
|
obj.passwordLabel = ' ' + i18n._('Basic Auth Password');
|
||||||
|
obj.password_required = false;
|
||||||
break;
|
break;
|
||||||
case 'mattermost':
|
case 'mattermost':
|
||||||
obj.mattermost_required = true;
|
obj.mattermost_required = true;
|
||||||
|
|||||||
@@ -218,6 +218,14 @@ https://gist.github.com/matburt/73bfbf85c2443f39d272
|
|||||||
The link below shows how to define an endpoint and parse headers and json content. It doesn't show how to configure Flask for HTTPS, but is fairly straightforward:
|
The link below shows how to define an endpoint and parse headers and json content. It doesn't show how to configure Flask for HTTPS, but is fairly straightforward:
|
||||||
http://flask.pocoo.org/snippets/111/
|
http://flask.pocoo.org/snippets/111/
|
||||||
|
|
||||||
|
You can also link an `httpbin` service to the development environment for testing webhooks using:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run --network="tools_default" --name httpbin -p 8204:80 kennethreitz/httpbin
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create an `httpbin` service reachable from the AWX container at `http://httpbin/post`, `http://httpbin/put`, etc. Outside of the container, you can reach the service at `http://localhost:8204`.
|
||||||
|
|
||||||
|
|
||||||
## Grafana
|
## Grafana
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user