From 0214390e6e9056762e1842eb31607cd644268bfa Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Wed, 10 May 2017 16:27:28 +0200 Subject: [PATCH 01/22] updating README --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 14af73a..688884f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Has been tested with Nagios, works well for us. Any Pagerduty Notifier using the [Icinga2 config](https://github.com/deathowl/OpenDuty-Icinga2) for openduty integration #Notifications -XMPP, email, SMS, Phone(Thanks Twilio for being awesome!), and Push notifications(thanks Pushover also),and Slack are supported at the moment. +XMPP, email, SMS, Phone(Thanks Twilio for being awesome!), Push notifications(thanks Pushover, Prowl as well!)and Slack, HipChat, Rocket.chat are supported at the moment. #Current status Openduty is in Beta status, it can be considered stable at the moment, however major structural changes can appear anytime (not affecting the API, or the Notifier structure) @@ -79,7 +79,22 @@ python manage.py runserver ``` now, you can start hacking on it. -# After models you've changed your models please run: +# Running as a service with systemd +*OpenDuty can be ran as a service with the help of gunicorn and systemd* +``` +cp systemd/gunicorn.service /etc/systemd/system/ +cp -r systemd/gunicorn.service.d /etc/systemd/system/gunicorn.service.d + +cp systemd/celery.service /etc/systemd/system/ +cp -r systemd/celery.service.d /etc/systemd/system/ + +systemctl daemon-reload +sudo systemctl start gunicorn +sudo systemctl enable gunicorn + +``` + +# After you've changed your models please run: ``` ./manage.py schemamigration openduty --auto ./manage.py schemamigration notification --auto From 463992fedcd778b71669c4b9a31fed240a28b288 Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Wed, 10 May 2017 16:28:15 +0200 Subject: [PATCH 02/22] add optionable send of notification for resolved incidents --- notification/helper.py | 4 +++- openduty/incidents.py | 5 ++++- openduty/models.py | 3 +++ openduty/services.py | 3 ++- openduty/templates/services/edit.html | 6 ++++++ openduty/templates/users/edit.html | 8 +++++++- openduty/users.py | 3 ++- 7 files changed, 27 insertions(+), 5 deletions(-) diff --git a/notification/helper.py b/notification/helper.py index 6fddbed..0470eab 100644 --- a/notification/helper.py +++ b/notification/helper.py @@ -1,7 +1,6 @@ __author__ = 'deathowl' from datetime import datetime, timedelta - from notification.tasks import send_notifications from openduty.escalation_helper import get_escalation_for_service from django.utils import timezone @@ -35,6 +34,9 @@ def generate_notifications_for_incident(incident): notifications = [] for officer_index, duty_officer in enumerate(duty_officers): + if incident.event_type == Incident.RESOLVE and not duty_officer.profile.send_resolve_enabled: + print "Skipping notification for %s because type is RESOLVE and user %s has send_resolve_enabled OFF" % (incident.incident_key, duty_officer.username) + continue escalation_time = incident.service_key.escalate_after * (officer_index + 1) escalate_at = current_time + timedelta(minutes=escalation_time) diff --git a/openduty/incidents.py b/openduty/incidents.py index c3e9917..4c0b682 100644 --- a/openduty/incidents.py +++ b/openduty/incidents.py @@ -205,6 +205,9 @@ def _update_type(user, ids, event_type): logmessage.save() if incident.event_type == Incident.RESOLVE or incident.event_type == Incident.ACKNOWLEDGE: ScheduledNotification.remove_all_for_incident(incident) + if incident.event_type == Incident.RESOLVE and service.send_resolve_enabled: + NotificationHelper.notify_incident(incident) + @login_required() @@ -317,4 +320,4 @@ def unsilence(request, incident_id): pass return HttpResponseRedirect(url) except Service.DoesNotExist: - raise Http404 \ No newline at end of file + raise Http404 diff --git a/openduty/models.py b/openduty/models.py index 736bcf4..bd5ad44 100644 --- a/openduty/models.py +++ b/openduty/models.py @@ -72,6 +72,8 @@ class Service(models.Model): policy = models.ForeignKey(SchedulePolicy, blank=True, null=True) escalate_after = models.IntegerField(blank=True, null=True) notifications_disabled = models.BooleanField(default=False) + send_resolve_enabled = models.BooleanField(default=False) + class Meta: verbose_name = _('service') @@ -223,6 +225,7 @@ class UserProfile(models.Model): prowl_application = models.CharField(max_length=256, blank=True) prowl_url = models.CharField(max_length=512, blank=True) rocket_webhook_url = models.CharField(max_length=512, blank=True) + send_resolve_enabled = models.BooleanField(default=False) class ServiceSilenced(models.Model): service = models.ForeignKey(Service) diff --git a/openduty/services.py b/openduty/services.py index 7bfb5ae..b666647 100644 --- a/openduty/services.py +++ b/openduty/services.py @@ -80,6 +80,7 @@ def save(request): service.escalate_after = request.POST['escalate_after'] service.retry = request.POST['retry'] service.notifications_disabled = request.POST.get("disable_notification", "off") == "on" + service.send_resolve_enabled = request.POST.get("send_resolve_notification", "off") == "on" if(request.POST['policy']): pol = SchedulePolicy.objects.get(id = request.POST['policy']) else: @@ -175,4 +176,4 @@ def unsilence(request, service_id): pass return HttpResponseRedirect(url) except Service.DoesNotExist: - raise Http404 \ No newline at end of file + raise Http404 diff --git a/openduty/templates/services/edit.html b/openduty/templates/services/edit.html index 6b86c54..006b6f3 100644 --- a/openduty/templates/services/edit.html +++ b/openduty/templates/services/edit.html @@ -78,6 +78,12 @@

+
+ +
+ +
+
diff --git a/openduty/templates/users/edit.html b/openduty/templates/users/edit.html index c8c08ff..18505aa 100644 --- a/openduty/templates/users/edit.html +++ b/openduty/templates/users/edit.html @@ -105,6 +105,12 @@
+
+ +
+ +
+
@@ -139,4 +145,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/openduty/users.py b/openduty/users.py index f015d27..2b8ed2a 100644 --- a/openduty/users.py +++ b/openduty/users.py @@ -86,6 +86,7 @@ def save(request): profile.prowl_application = request.POST['prowl_application'] profile.prowl_url = request.POST['prowl_url'] profile.rocket_webhook_url = request.POST['rocket_webhook_url'] + profile.send_resolve_enabled = request.POST.get("send_resolve_notification", "off") == "on" profile.save() return HttpResponseRedirect(reverse('openduty.users.list')) @@ -101,4 +102,4 @@ def save(request): def testnotification(request): user = User.objects.get(id=request.POST['id']) NotificationHelper.notify_user_about_incident(None, user, 1, "This is a notification test message, just ignore it") - return HttpResponseRedirect(reverse('openduty.users.list')) \ No newline at end of file + return HttpResponseRedirect(reverse('openduty.users.list')) From 40ab5ad15a20dbc67339a134e09291cdfd4ba868 Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Wed, 10 May 2017 16:58:22 +0200 Subject: [PATCH 03/22] add HipChat notification method --- notification/models.py | 3 +- notification/notifier/hipchat.py | 65 ++++++++++++++++++++++++++++++ notification/tasks.py | 7 +++- openduty/models.py | 2 + openduty/templates/users/edit.html | 30 +++++++++++--- openduty/users.py | 8 +++- 6 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 notification/notifier/hipchat.py diff --git a/notification/models.py b/notification/models.py index a395970..8168255 100644 --- a/notification/models.py +++ b/notification/models.py @@ -21,9 +21,10 @@ class UserNotificationMethod(models.Model): METHOD_SLACK = 'slack' METHOD_PROWL = 'prowl' METHOD_ROCKET = 'rocket' + METHOD_HIPCHAT = 'hipchat' - methods = [METHOD_XMPP, METHOD_PUSHOVER, METHOD_EMAIL, METHOD_TWILIO_SMS, METHOD_TWILIO_CALL, METHOD_SLACK, METHOD_PROWL, METHOD_ROCKET] + methods = [METHOD_XMPP, METHOD_PUSHOVER, METHOD_EMAIL, METHOD_TWILIO_SMS, METHOD_TWILIO_CALL, METHOD_SLACK, METHOD_PROWL, METHOD_ROCKET, METHOD_HIPCHAT] user = models.ForeignKey(User, related_name='notification_methods') position = models.IntegerField() diff --git a/notification/notifier/hipchat.py b/notification/notifier/hipchat.py new file mode 100644 index 0000000..7cdff63 --- /dev/null +++ b/notification/notifier/hipchat.py @@ -0,0 +1,65 @@ +from hypchat import HypChat +import json +import requests + +class HipchatNotifier: + + def __init__(self, config): + self.__config = config + + def start(self): + if not self.__config['token']: + return False + elif self.__config['endpoint']: + return HypChat(self.__config['token'], endpoint=self.__config['endpoint']) + else: + return HypChat(self.__config['token']) + + def notify(self, notification): + hc = self.start() + try: + description = notification.incident.description + details = notification.incident.details + except : + description = notification.message + details = "" + try: + message = description + " " + details + colour = "yellow" + if "CRITICAL" in message: + colour = "red" + elif "UNKNOWN" in message: + colour = "gray" + if notification.user_to_notify.profile.hipchat_room_name: + print "Notifying HipChat via API v2" + response = hc.get_room(notification.user_to_notify.profile.hipchat_room_name).notification(message, color=colour, notify="True", format="html") + elif notification.user_to_notify.profile.hipchat_room_url: + print "Notifying HipChat via a simple POST" + headers = {"content-type": "application/json"} + hip_msg = '{"color": "' + colour + '", "message": "' + message + '", "notify": true, "message_format": "html"}' + response = requests.post(notification.user_to_notify.profile.hipchat_room_url,headers=headers,data=hip_msg) + print response.content + else: + print "HipChat message send failed" + return + print "HipChat message sent" + except Exception, e: + try: + resp = json.loads(str(e)) + print "Failed to send HipChat message %s " % (e, str(resp['error']['code']) + " " + resp['error']['message']) + raise + except ValueError, e2: + print "Failed to send HipChat message and failed to get it's error %s ; %s" % (e, e2) + raise + + def get_all_rooms(self): + hc = self.start() + if not hc: + return [""] + else: + rooms = hc.rooms().contents() + names = [""] + for room in rooms: + names.append(room['name']) + return names + diff --git a/notification/tasks.py b/notification/tasks.py index 788dc67..034f5a2 100644 --- a/notification/tasks.py +++ b/notification/tasks.py @@ -1,6 +1,5 @@ from __future__ import absolute_import -from notification.notifier.rocket import RocketNotifier from openduty.celery import app from notification.notifier.pushover import PushoverNotifier from notification.notifier.xmpp import XmppNotifier @@ -9,7 +8,8 @@ from notification.notifier.twilio_call import TwilioCallNotifier from notification.notifier.slack import SlackNotifier from notification.notifier.prowl import ProwlNotifier - +from notification.notifier.rocket import RocketNotifier +from notification.notifier.hipchat import HipchatNotifier from notification.models import ScheduledNotification, UserNotificationMethod from django.conf import settings from django.utils import timezone @@ -36,6 +36,9 @@ def send_notifications(notification_id): notifier = ProwlNotifier(settings.PROWL_SETTINGS) elif notification.notifier == UserNotificationMethod.METHOD_ROCKET: notifier = RocketNotifier() + elif notification.notifier == UserNotificationMethod.METHOD_HIPCHAT: + notifier = HipchatNotifier(settings.HIPCHAT_SETTINGS) + notifier.notify(notification) # Log successful notification logmessage = EventLog() diff --git a/openduty/models.py b/openduty/models.py index bd5ad44..08b78b1 100644 --- a/openduty/models.py +++ b/openduty/models.py @@ -225,6 +225,8 @@ class UserProfile(models.Model): prowl_application = models.CharField(max_length=256, blank=True) prowl_url = models.CharField(max_length=512, blank=True) rocket_webhook_url = models.CharField(max_length=512, blank=True) + hipchat_room_name = models.CharField(max_length=100) + hipchat_room_url = models.CharField(max_length=100) send_resolve_enabled = models.BooleanField(default=False) class ServiceSilenced(models.Model): diff --git a/openduty/templates/users/edit.html b/openduty/templates/users/edit.html index 18505aa..5dd5a97 100644 --- a/openduty/templates/users/edit.html +++ b/openduty/templates/users/edit.html @@ -106,16 +106,36 @@
- -
- + + +
+
- +
- + +
+
+
+ + +
+ +
+
+
+ +
+
diff --git a/openduty/users.py b/openduty/users.py index 2b8ed2a..4e77a4c 100644 --- a/openduty/users.py +++ b/openduty/users.py @@ -3,6 +3,7 @@ __author__ = "dzsubek" from notification.models import UserNotificationMethod +from notification.notifier.hipchat import HipchatNotifier from django.http import HttpResponseRedirect from django.template.response import TemplateResponse from django.contrib.auth.decorators import login_required @@ -12,6 +13,7 @@ from django.db import IntegrityError from django.core.urlresolvers import reverse from django.contrib import messages +from django.conf import settings @login_required() def list(request): @@ -35,14 +37,14 @@ def edit(request, id): return TemplateResponse( request, 'users/edit.html', - {'item': user, 'methods': UserNotificationMethod.methods, 'user_methods': user_methods, 'empty_user_method': UserNotificationMethod()} + {'item': user, 'methods': UserNotificationMethod.methods, 'user_methods': user_methods, 'empty_user_method': UserNotificationMethod(), 'hipchat_rooms': HipchatNotifier(settings.HIPCHAT_SETTINGS).get_all_rooms()} ) except User.DoesNotExist: raise Http404 @login_required() def new(request): - return TemplateResponse(request, 'users/edit.html', {'methods': UserNotificationMethod.methods, 'empty_user_method': UserNotificationMethod()}) + return TemplateResponse(request, 'users/edit.html', {'methods': UserNotificationMethod.methods, 'empty_user_method': UserNotificationMethod(), 'hipchat_rooms': HipchatNotifier(settings.HIPCHAT_SETTINGS).get_all_rooms()}) @login_required() @require_http_methods(["POST"]) @@ -86,6 +88,8 @@ def save(request): profile.prowl_application = request.POST['prowl_application'] profile.prowl_url = request.POST['prowl_url'] profile.rocket_webhook_url = request.POST['rocket_webhook_url'] + profile.hipchat_room_name = request.POST['hipchat_room_name'] + profile.hipchat_room_url = request.POST['hipchat_room_url'] profile.send_resolve_enabled = request.POST.get("send_resolve_notification", "off") == "on" profile.save() From c471874c55d1e1b5ccb0c58462a0c8a733900417 Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Wed, 10 May 2017 17:00:03 +0200 Subject: [PATCH 04/22] add better notification logging and result keeping --- notification/tasks.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/notification/tasks.py b/notification/tasks.py index 034f5a2..77fd447 100644 --- a/notification/tasks.py +++ b/notification/tasks.py @@ -16,7 +16,7 @@ from openduty.models import EventLog -@app.task(ignore_result=True) +@app.task(ignore_result=False) def send_notifications(notification_id): try: notification = ScheduledNotification.objects.get(id = notification_id) @@ -47,7 +47,7 @@ def send_notifications(notification_id): logmessage.incident_key = notification.incident logmessage.user = notification.user_to_notify logmessage.action = 'notified' - logmessage.data = "Notification sent to %s about %s service" % (notification.user_to_notify, logmessage.service_key, ) + logmessage.data = "Notification sent to %s about %s service via %s" % (notification.user_to_notify, logmessage.service_key, notification.notifier, ) logmessage.occurred_at = timezone.now() logmessage.save() if notification.notifier != UserNotificationMethod.METHOD_TWILIO_CALL: @@ -55,7 +55,7 @@ def send_notifications(notification_id): notification.delete() except ScheduledNotification.DoesNotExist: pass #Incident was resolved. NOP. - except: + except Exception, e: # Log successful notification logmessage = EventLog() if notification.incident: @@ -63,8 +63,9 @@ def send_notifications(notification_id): logmessage.incident_key = notification.incident logmessage.user = notification.user_to_notify logmessage.action = 'notification_failed' - logmessage.data = "Sending notification failed to %s about %s service" % (notification.user_to_notify, logmessage.service_key, ) + logmessage.data = "Sending notification failed to %s about %s service because %s" % (notification.user_to_notify, logmessage.service_key, e,) logmessage.occurred_at = timezone.now() logmessage.save() + return (logmessage.data) raise From dba9d0cf719246e3e56bb279da8f1b467db811aa Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Wed, 10 May 2017 17:06:15 +0200 Subject: [PATCH 05/22] add unacknowledge backend code --- openduty/models.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openduty/models.py b/openduty/models.py index 08b78b1..e9930fc 100644 --- a/openduty/models.py +++ b/openduty/models.py @@ -91,6 +91,7 @@ class EventLog(models.Model): Event Log """ ACTIONS = (('acknowledge', 'acknowledge'), + ('unacknowledge', 'unacknowledge'), ('resolve', 'resolve'), ('silence_service', 'silence service'), ('unsilence_service', 'unsilence service'), @@ -105,6 +106,7 @@ class EventLog(models.Model): @property def color(self): colort_dict = {'acknowledge': 'warning', + 'unacknowledge' : 'warning', 'resolve': 'success', 'silence_service': 'active', 'unsilence_service': 'active', @@ -139,6 +141,7 @@ class Incident(models.Model): TRIGGER = "trigger" RESOLVE = "resolve" ACKNOWLEDGE = "acknowledge" + UNACKNOWLEDGE = "unacknowledge" """ Incidents are representations of a malfunction in the system. """ @@ -152,6 +155,7 @@ class Incident(models.Model): @property def color(self): colort_dict = {'acknowledge': 'warning', + 'unacknowledge': 'warning', 'resolve': 'success', 'silence_service': 'active', 'silence_incident': 'active', @@ -171,8 +175,8 @@ def __str__(self): def natural_key(self): return (self.service_key, self.incident_key) def clean(self): - if self.event_type not in ['trigger', 'acknowledge', 'resolve']: - raise ValidationError("'%s' is an invalid event type, valid values are 'trigger', 'acknowledge' and 'resolve'" % self.event_type) + if self.event_type not in ['trigger', 'acknowledge', 'unacknowledge', 'resolve']: + raise ValidationError("'%s' is an invalid event type, valid values are 'trigger', 'acknowledge', 'unacknowledge' and 'resolve'" % self.event_type) @python_2_unicode_compatible class ServiceTokens(models.Model): From 2359793f175829483317cd9e8292c8216693042d Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Wed, 10 May 2017 17:09:00 +0200 Subject: [PATCH 06/22] update requirements.txt for hipchat, django-debug-toolbar and security patches on gunicorn and vobject --- requirements.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c105cf5..612e4b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ django-bootstrap3==6.1.0 dnspython==1.12.0 fpconst==0.7.2 future==0.14.3 -gunicorn==19.3.0 +gunicorn==19.6.0 httplib2==0.9.1 icalendar==3.9.0 kombu~=3.0 @@ -33,6 +33,8 @@ six==1.9.0 slacker==0.6.2 sleekxmpp==1.3.1 twilio==4.4.0 -vobject +vobject==0.9.1 wsgiref==0.1.2 django-twilio +hypchat +django-debug-toolbar From d45c1af53e660e6229745a7a4713761d5ebec248 Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Wed, 10 May 2017 17:16:08 +0200 Subject: [PATCH 07/22] add incident endpoint for API as an alias to create_event --- openduty/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openduty/urls.py b/openduty/urls.py index 23f1b0d..920f267 100644 --- a/openduty/urls.py +++ b/openduty/urls.py @@ -17,6 +17,7 @@ rest_router.register(r'schedule_policies', views.SchedulePolicyViewSet) rest_router.register(r'schedule_policy_rules', views.SchedulePolicyRuleViewSet) rest_router.register(r'create_event', incidents.IncidentViewSet) +rest_router.register(r'incident', incidents.IncidentViewSet) rest_router.register(r'healthcheck', healthcheck.HealthCheckViewSet) rest_router.register(r'celeryhealthcheck', healthcheck.CeleryHealthCheckViewSet) rest_router.register(r'opsweekly', opsweekly.OpsWeeklyIncidentViewSet) From 30a1442441431bd5c59ca1b167e96ffc1048ce4e Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Wed, 10 May 2017 17:19:04 +0200 Subject: [PATCH 08/22] add django-debug-toolbar to settings and urls if DEBUG==True --- openduty/settings.py | 3 ++- openduty/urls.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/openduty/settings.py b/openduty/settings.py index 97e4b78..d32ef96 100644 --- a/openduty/settings.py +++ b/openduty/settings.py @@ -52,7 +52,8 @@ 'django_tables2', 'django_tables2_simplefilter', 'bootstrap3', - "django_twilio" + "django_twilio", + "debug_toolbar" ) TEMPLATE_CONTEXT_PROCESSORS = ( diff --git a/openduty/urls.py b/openduty/urls.py index 920f267..dddca51 100644 --- a/openduty/urls.py +++ b/openduty/urls.py @@ -115,3 +115,12 @@ urlpatterns += patterns('', (r'^static/(?P.*)$', 'django.views.static.serve', {'document_root': settings.STATIC_ROOT, 'show_indexes':True}), ) + + + +if settings.DEBUG: + import debug_toolbar + urlpatterns += [ + url(r'^__debug__/', include(debug_toolbar.urls)), + ] + From 1b2eaeaf5e3c32d01acb0e948e51cbdaf261df0f Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Wed, 10 May 2017 17:24:42 +0200 Subject: [PATCH 09/22] add better error management for hipchat and default settings.py config --- notification/notifier/hipchat.py | 25 ++++++++++++++----------- openduty/settings.py | 6 ++++++ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/notification/notifier/hipchat.py b/notification/notifier/hipchat.py index 7cdff63..0f46232 100644 --- a/notification/notifier/hipchat.py +++ b/notification/notifier/hipchat.py @@ -8,8 +8,9 @@ def __init__(self, config): self.__config = config def start(self): - if not self.__config['token']: - return False + if not self.__config['token'] or not self.__config['endpoint']: + print "HipChat configuration is missing %s" % self.__config + raise elif self.__config['endpoint']: return HypChat(self.__config['token'], endpoint=self.__config['endpoint']) else: @@ -53,13 +54,15 @@ def notify(self, notification): raise def get_all_rooms(self): - hc = self.start() - if not hc: - return [""] - else: - rooms = hc.rooms().contents() - names = [""] - for room in rooms: - names.append(room['name']) + try: + hc = self.start() + if not hc: + return [""] + else: + rooms = hc.rooms().contents() + names = [] + for room in rooms: + names.append(room['name']) return names - + except: + return [""] diff --git a/openduty/settings.py b/openduty/settings.py index d32ef96..e24d0d1 100644 --- a/openduty/settings.py +++ b/openduty/settings.py @@ -132,6 +132,12 @@ PROWL_SETTINGS = { } +HIPCHAT_SETTINGS = { + 'token' : '', + 'endpoint' : '' +} + + CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', From b6fe5929f49042edb16df7fccdc748826ca37f73 Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Wed, 10 May 2017 17:34:35 +0200 Subject: [PATCH 10/22] fix email notifier to be able to use custom SMTP servers; add default settings.py config --- notification/notifier/email.py | 11 +++++++---- openduty/settings.py | 4 ++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/notification/notifier/email.py b/notification/notifier/email.py index a0a7c66..60135be 100644 --- a/notification/notifier/email.py +++ b/notification/notifier/email.py @@ -10,8 +10,10 @@ def __init__(self, config): self.__config = config def notify(self, notification): - gmail_user = self.__config['user'] - gmail_pwd = self.__config['password'] + host = self.__config['host'] + port = self.__config['port'] + user = self.__config['user'] + password = self.__config['password'] truncate_length = int(self.__config.get('max_subject_length', 100)) FROM = self.__config['user'] TO = [notification.user_to_notify.email] @@ -25,10 +27,11 @@ def notify(self, notification): message = """\From: %s\nTo: %s\nSubject: %s\n\n%s """ % (FROM, ", ".join(TO), SUBJECT, TEXT) try: - server = smtplib.SMTP("smtp.gmail.com", 587) + server = smtplib.SMTP(host, int(port)) server.starttls() server.ehlo() - server.login(gmail_user, gmail_pwd) + if user and password: + server.login(user, password) server.sendmail(FROM, TO, message) server.close() print 'successfully sent the mail' diff --git a/openduty/settings.py b/openduty/settings.py index e24d0d1..50732c5 100644 --- a/openduty/settings.py +++ b/openduty/settings.py @@ -121,6 +121,10 @@ } EMAIL_SETTINGS = { + 'user': '', + 'password': '', + 'host': '', + 'port': '' } TWILIO_SETTINGS = { From 8b7d61c9c08ada2b34ffd18c9290c786a5408fb9 Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Wed, 10 May 2017 17:36:06 +0200 Subject: [PATCH 11/22] modify user notif_method template for better visibility --- openduty/templates/users/notification_method.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openduty/templates/users/notification_method.html b/openduty/templates/users/notification_method.html index 9e78be5..9a67c81 100644 --- a/openduty/templates/users/notification_method.html +++ b/openduty/templates/users/notification_method.html @@ -1,6 +1,6 @@
  • - -
    + +
    -
  • \ No newline at end of file + From 116cf33497aedbd8a0f6e14ea94e62eccafa1cdc Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Wed, 10 May 2017 18:32:22 +0200 Subject: [PATCH 12/22] front-end: add cosmetic glyphicons to menus; add confirmation before deleting users,services,schedules; add pseudo-ACLs(only staff and user himself can modify/test notification on a user; only staff can delete/create new users) --- openduty/templates/base.html | 18 ++++----- openduty/templates/escalation/list.html | 2 +- openduty/templates/incidents/details.html | 10 ++++- openduty/templates/incidents/list2.html | 7 ++-- openduty/templates/schedule/list.html | 2 +- openduty/templates/services/edit.html | 5 +++ openduty/templates/services/list.html | 2 +- openduty/templates/users/list.html | 48 ++++++++++------------- openduty/urls.py | 2 + openduty/users.py | 11 ++++++ 10 files changed, 64 insertions(+), 43 deletions(-) diff --git a/openduty/templates/base.html b/openduty/templates/base.html index 2bfdfe7..ba1135a 100644 --- a/openduty/templates/base.html +++ b/openduty/templates/base.html @@ -30,9 +30,9 @@
    - \ No newline at end of file + diff --git a/openduty/templates/escalation/list.html b/openduty/templates/escalation/list.html index c4db408..fae6d03 100644 --- a/openduty/templates/escalation/list.html +++ b/openduty/templates/escalation/list.html @@ -28,7 +28,7 @@ data-toggle="tooltip" data-placement="top" title="Edit"> + data-toggle="tooltip" data-placement="top" title="Remove" input type="submit" onclick="return confirm('Are you sure you want to delete this escalation policy: {{item.name}}')" value="Button"> diff --git a/openduty/templates/incidents/details.html b/openduty/templates/incidents/details.html index c96e7c5..2da203a 100644 --- a/openduty/templates/incidents/details.html +++ b/openduty/templates/incidents/details.html @@ -3,6 +3,14 @@ {% load arrowfilter %} {% block content %} +{% if messages %} +
    + {% for message in messages %} + {{ message }} + {% endfor %} +
    +{% endif %} +

    Incident details

    @@ -142,4 +150,4 @@
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/openduty/templates/incidents/list2.html b/openduty/templates/incidents/list2.html index 811fff0..0949390 100644 --- a/openduty/templates/incidents/list2.html +++ b/openduty/templates/incidents/list2.html @@ -33,9 +33,10 @@
    - - - +
    + + +
    {% csrf_token %}
    {% render_table table %} diff --git a/openduty/templates/schedule/list.html b/openduty/templates/schedule/list.html index 641736f..b92d9cb 100644 --- a/openduty/templates/schedule/list.html +++ b/openduty/templates/schedule/list.html @@ -27,7 +27,7 @@ - + diff --git a/openduty/templates/services/edit.html b/openduty/templates/services/edit.html index 006b6f3..09a81de 100644 --- a/openduty/templates/services/edit.html +++ b/openduty/templates/services/edit.html @@ -92,6 +92,9 @@

    {% csrf_token %} +
    +

    Silence service

    +
    @@ -103,7 +106,9 @@

    +
    +
    {% if item %}

    API Keys

    diff --git a/openduty/templates/services/list.html b/openduty/templates/services/list.html index 4b6cab0..ccd0815 100644 --- a/openduty/templates/services/list.html +++ b/openduty/templates/services/list.html @@ -36,7 +36,7 @@ data-toggle="tooltip" data-placement="top" title="Edit"> + data-toggle="tooltip" data-placement="top" title="Delete" input type="submit" onclick="return confirm('Are you sure you want to delete this service : {{ item.name }}')" value="Button"> diff --git a/openduty/templates/users/list.html b/openduty/templates/users/list.html index f6a68f4..f74d93b 100644 --- a/openduty/templates/users/list.html +++ b/openduty/templates/users/list.html @@ -3,7 +3,9 @@ {% block content %}

    Users

    + {% if perms.is_staff %} + {% endif %}
    @@ -15,37 +17,29 @@ - {% for item in users %} - - - - - + + + + + {% endif %} {% endfor %}
    {{ item.username }}{{ item.email }}{{ item.profile.phone_number }} -
    + {% for item in users %} + {% if perms.is_staff or item.id == user.id %} +
    {{ item.username }}{{ item.email }}{{ item.profile.phone_number }} + - - - {% if item.id != user.id %} - - - {% else %} - - - {% csrf_token %} - - - {% endif %} - + + + + {% csrf_token %} + + {% if item.id != user.id %} + + {% endif %} +
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/openduty/urls.py b/openduty/urls.py index dddca51..ae34d0c 100644 --- a/openduty/urls.py +++ b/openduty/urls.py @@ -10,6 +10,7 @@ from django_tables2_simplefilter import FilteredSingleTableView from .incidents import ServicesByMe + admin.autodiscover() rest_router = rest_routers.SimpleRouter(trailing_slash=False) rest_router.register(r'users', views.UserViewSet) @@ -124,3 +125,4 @@ url(r'^__debug__/', include(debug_toolbar.urls)), ] + diff --git a/openduty/users.py b/openduty/users.py index 4e77a4c..daa3b14 100644 --- a/openduty/users.py +++ b/openduty/users.py @@ -14,6 +14,9 @@ from django.core.urlresolvers import reverse from django.contrib import messages from django.conf import settings +from django.contrib.admin.views.decorators import staff_member_required +from django.core.exceptions import PermissionDenied + @login_required() def list(request): @@ -21,6 +24,7 @@ def list(request): return TemplateResponse(request, 'users/list.html', {'users': users}) @login_required() +@staff_member_required def delete(request, id): try: user = User.objects.get(id = id) @@ -31,6 +35,8 @@ def delete(request, id): @login_required() def edit(request, id): + if not request.user.is_staff and int(request.user.id) != int(id): + raise PermissionDenied("User " + str(request.user.id) + " isn't staff") try: user = User.objects.get(id = id) user_methods = UserNotificationMethod.objects.filter(user = user).order_by('position') @@ -43,12 +49,15 @@ def edit(request, id): raise Http404 @login_required() +@staff_member_required def new(request): return TemplateResponse(request, 'users/edit.html', {'methods': UserNotificationMethod.methods, 'empty_user_method': UserNotificationMethod(), 'hipchat_rooms': HipchatNotifier(settings.HIPCHAT_SETTINGS).get_all_rooms()}) @login_required() @require_http_methods(["POST"]) def save(request): + if not request.user.is_staff and int(request.user.id) != int(request.POST['id']): + raise PermissionDenied("User " + str(request.user.id) + " isn't staff") try: user = User.objects.get(id = request.POST['id']) except User.DoesNotExist: @@ -104,6 +113,8 @@ def save(request): @login_required() @require_http_methods(["POST"]) def testnotification(request): + if not request.user.is_staff and int(request.user.id) != int(request.POST['id']): + raise PermissionDenied("User " + str(request.user.id) + " isn't staff") user = User.objects.get(id=request.POST['id']) NotificationHelper.notify_user_about_incident(None, user, 1, "This is a notification test message, just ignore it") return HttpResponseRedirect(reverse('openduty.users.list')) From bde63412089b31d629ace89aed0e9123d420890e Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Thu, 11 May 2017 18:03:40 +0200 Subject: [PATCH 13/22] add optional TLS for SMTP email notifier --- notification/notifier/email.py | 3 ++- openduty/settings.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/notification/notifier/email.py b/notification/notifier/email.py index 60135be..0a5b33b 100644 --- a/notification/notifier/email.py +++ b/notification/notifier/email.py @@ -28,7 +28,8 @@ def notify(self, notification): """ % (FROM, ", ".join(TO), SUBJECT, TEXT) try: server = smtplib.SMTP(host, int(port)) - server.starttls() + if self.__config['tls']: + server.starttls() server.ehlo() if user and password: server.login(user, password) diff --git a/openduty/settings.py b/openduty/settings.py index 50732c5..b269971 100644 --- a/openduty/settings.py +++ b/openduty/settings.py @@ -124,7 +124,8 @@ 'user': '', 'password': '', 'host': '', - 'port': '' + 'port': '', + 'tls': False } TWILIO_SETTINGS = { From 1b54188768353b5f1292ef8d408c60ca818e06a5 Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Fri, 12 May 2017 11:08:32 +0200 Subject: [PATCH 14/22] add UNACK support in incidents.py --- openduty/incidents.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openduty/incidents.py b/openduty/incidents.py index 4c0b682..663c467 100644 --- a/openduty/incidents.py +++ b/openduty/incidents.py @@ -24,9 +24,7 @@ from openduty.tasks import unsilence_incident import uuid import base64 - from .tables import IncidentTable - from django_tables2_simplefilter import FilteredSingleTableView class IncidentViewSet(viewsets.ModelViewSet): @@ -50,7 +48,7 @@ def is_relevant(self, incident, new_event_type): # True if not acknowleged or type is resolve return (incident.event_type != Incident.ACKNOWLEDGE or (incident.event_type == Incident.ACKNOWLEDGE and - new_event_type == Incident.RESOLVE)) + new_event_type == Incident.RESOLVE) or (incident.event_type == Incident.ACKNOWLEDGE and new_event_type == Incident.UNACKNOWLEDGE)) # New incident else: # True if this is a trigger action From fb706ddbdd448d9456dbcdb3abcd684ec67037b7 Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Fri, 12 May 2017 11:08:50 +0200 Subject: [PATCH 15/22] notification tasks return result for easier debugging with Flower --- notification/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/tasks.py b/notification/tasks.py index 77fd447..7a119f9 100644 --- a/notification/tasks.py +++ b/notification/tasks.py @@ -50,6 +50,7 @@ def send_notifications(notification_id): logmessage.data = "Notification sent to %s about %s service via %s" % (notification.user_to_notify, logmessage.service_key, notification.notifier, ) logmessage.occurred_at = timezone.now() logmessage.save() + return (logmessage.data) if notification.notifier != UserNotificationMethod.METHOD_TWILIO_CALL: # In case of a twilio call, we need the object for TWiml generation notification.delete() @@ -68,4 +69,3 @@ def send_notifications(notification_id): logmessage.save() return (logmessage.data) raise - From aae607b0a7a5e53992c9b5e5d9e374db4c4e8c55 Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Fri, 12 May 2017 11:43:35 +0200 Subject: [PATCH 16/22] add more verbose errors from incidents.py and send resolve notifications --- openduty/incidents.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openduty/incidents.py b/openduty/incidents.py index 663c467..6f4d93b 100644 --- a/openduty/incidents.py +++ b/openduty/incidents.py @@ -60,9 +60,9 @@ def create(self, request, *args, **kwargs): serviceToken = ServiceTokens.objects.get(token_id=token) service = serviceToken.service_id except ServiceTokens.DoesNotExist: - return Response({}, status=status.HTTP_404_NOT_FOUND) + return Response({"Service key does not exist"}, status=status.HTTP_404_NOT_FOUND) except Token.DoesNotExist: - return Response({}, status=status.HTTP_403_FORBIDDEN) + return Response({"No service key"}, status=status.HTTP_403_FORBIDDEN) with transaction.atomic(): try: @@ -125,8 +125,11 @@ def create(self, request, *args, **kwargs): service=service).count() > 0 if incident.event_type == Incident.TRIGGER and not servicesilenced: NotificationHelper.notify_incident(incident) - if incident.event_type == "resolve" or incident.event_type == Incident.ACKNOWLEDGE: + if incident.event_type == Incident.RESOLVE or incident.event_type == Incident.ACKNOWLEDGE: ScheduledNotification.remove_all_for_incident(incident) + if incident.event_type == Incident.RESOLVE and service.send_resolve_enabled: + NotificationHelper.notify_incident(incident) + headers = self.get_success_headers(request.POST) From bab0a4a477b4fad088ef3da70ec6469b2427aa44 Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Fri, 12 May 2017 12:16:41 +0200 Subject: [PATCH 17/22] add escalate_to_service: the possibility to escalate(basically send, transfer) an incident to another service so that it's on-call,fallback etc. be notified as well --- openduty/incidents.py | 122 +++++++++++++++++++++++++++++++++++------- 1 file changed, 103 insertions(+), 19 deletions(-) diff --git a/openduty/incidents.py b/openduty/incidents.py index 6f4d93b..a0aeb5e 100644 --- a/openduty/incidents.py +++ b/openduty/incidents.py @@ -13,6 +13,7 @@ from .serializers import IncidentSerializer from rest_framework import status from rest_framework.response import Response +from rest_framework.decorators import detail_route, list_route from django.http import HttpResponseRedirect from django.contrib.auth.decorators import login_required from django.template.response import TemplateResponse @@ -25,6 +26,7 @@ import uuid import base64 from .tables import IncidentTable + from django_tables2_simplefilter import FilteredSingleTableView class IncidentViewSet(viewsets.ModelViewSet): @@ -66,13 +68,27 @@ def create(self, request, *args, **kwargs): with transaction.atomic(): try: - incident = Incident.objects.get( - incident_key=request.DATA["incident_key"], - service_key=service) - - event_log_message = "%s api key changed %s from %s to %s" % ( - serviceToken.name, incident.incident_key, - incident.event_type, request.DATA['event_type']) + esc = False + incident = Incident.objects.get(incident_key=request.DATA["incident_key"],service_key=service) + print "Received %s for %s on service %s" % (request.DATA['event_type'],request.DATA['incident_key'],serviceToken.name) + #check if type is ACK or resolve and if there's an escalation to a different escalation policy, remove it + if request.DATA['event_type'] == Incident.ACKNOWLEDGE or request.DATA['event_type'] == Incident.RESOLVE: + print "ACK or Resolve, removing specific escalation" + esc = True + incident.service_to_escalate_to = None + incident.save() + # check if incident is resolved and refuse to ACK + if not (incident.event_type == Incident.RESOLVE and request.DATA['event_type'] == Incident.ACKNOWLEDGE): + event_log_message = "%s api key changed %s from %s to %s" % ( + serviceToken.name, incident.incident_key, + incident.event_type, request.DATA['event_type']) + if esc: + event_log_message += ", unescalated" + else: + response = {} + response["status"] = "failure" + response["message"] = "Can\'t ACK a resolved incident!" + return Response(response, status=status.HTTP_400_BAD_REQUEST) except (Incident.DoesNotExist, KeyError): incident = Incident() try: @@ -130,17 +146,72 @@ def create(self, request, *args, **kwargs): if incident.event_type == Incident.RESOLVE and service.send_resolve_enabled: NotificationHelper.notify_incident(incident) - headers = self.get_success_headers(request.POST) response = {} response["status"] = "success" response["message"] = "Event processed" response["incident_key"] = incident.incident_key - return Response( - response, - status=status.HTTP_201_CREATED, - headers=headers) + return Response(response,status=status.HTTP_201_CREATED,headers=headers) + + + #escalate an incident to another service's escalation rule; persists until ACK + @detail_route(methods=['put']) + def escalate(self, request, *args, **kwargs): + #get arguments + try: + token = Token.objects.get(key=request.DATA["service_key"]) + serviceToken = ServiceTokens.objects.get(token_id=token) + service = serviceToken.service_id + except ServiceTokens.DoesNotExist: + return Response({"Service key does not exist"}, status=status.HTTP_404_NOT_FOUND) + except Token.DoesNotExist: + return Response({"No service key"}, status=status.HTTP_403_FORBIDDEN) + + try: + token2 = Token.objects.get(key=request.DATA["service_key_to_escalate_to"]) + serviceToken2 = ServiceTokens.objects.get(token_id=token2) + service2 = serviceToken2.service_id + except ServiceTokens.DoesNotExist: + return Response({"Service to escalate to key does not exist"}, status=status.HTTP_404_NOT_FOUND) + except Token.DoesNotExist: + return Response({"No service to escalate to key"}, status=status.HTTP_403_FORBIDDEN) + + #modify incident + with transaction.atomic(): + try: + # get service_to_escalate to and modify incident object + incident = Incident.objects.get(incident_key=request.DATA["incident_key"],service_key=service) + incident.service_to_escalate_to = service2 + incident.event_type = "escalated" + if request.DATA["incident_details"]: + incident.details = request.DATA["incident_details"] +# incident.description = "[escalated] " + incident.description + incident.save() + + event_log_message = "%s escalated to service escalation policy : %s to %s" % (request.user.username, incident.incident_key, service2.name) + event_log = EventLog() + event_log.user = request.user + event_log.action = "escalate" + event_log.incident_key = incident + event_log.service_key = incident.service_key + event_log.data = event_log_message + event_log.occurred_at = timezone.now() + event_log.save() + + except (Incident.DoesNotExist, KeyError): + return Response({"Incident does not exist"}, status=status.HTTP_404_NOT_FOUND) + except (Service.DoesNotExist, KeyError): + return Response({"Service does not exist"}, status=status.HTTP_400_BAD_REQUEST) + # remove all planned notifs + ScheduledNotification.remove_all_for_incident(incident) + # notify anew, this time notify_incident will detect the service_to_escalate to and notify its escalation rule + NotificationHelper.notify_incident(incident) + + headers = self.get_success_headers(request.POST) + + return Response({"Incident successfully escalated to service " + service2.name + " escalation policy"},status=status.HTTP_200_OK,headers=headers) + class ServicesByMe(FilteredSingleTableView): model = Incident @@ -186,7 +257,10 @@ def _update_type(user, ids, event_type): for incident_id in ids: with transaction.atomic(): incident = Incident.objects.get(id=int(incident_id)) - + unesc = False + if incident.service_to_escalate_to is not None: + incident.service_to_escalate_to = None + unesc = True logmessage = EventLog() logmessage.service_key = incident.service_key logmessage.user = user @@ -196,6 +270,8 @@ def _update_type(user, ids, event_type): incident.incident_key, incident.event_type, event_type) + if unesc: + logmessage.data += ", unescalated" logmessage.occurred_at = timezone.now() incident.event_type = event_type @@ -206,9 +282,6 @@ def _update_type(user, ids, event_type): logmessage.save() if incident.event_type == Incident.RESOLVE or incident.event_type == Incident.ACKNOWLEDGE: ScheduledNotification.remove_all_for_incident(incident) - if incident.event_type == Incident.RESOLVE and service.send_resolve_enabled: - NotificationHelper.notify_incident(incident) - @login_required() @@ -217,16 +290,27 @@ def update_type(request): event_type = request.POST['event_type'] event_types = ('acknowledge', 'resolve') incident_ids = request.POST.getlist('selection', None) - if not event_type: messages.error(request, 'Invalid event modification!') return HttpResponseRedirect(request.POST['url']) try: if incident_ids: - _update_type(request.user, incident_ids, event_type) + for id in incident_ids: + with transaction.atomic(): + incident = Incident.objects.get(id=id) + if incident.event_type == 'resolve' and event_type == 'acknowledge': + messages.error(request, 'Can\' ACK a resolved incident!') + return HttpResponseRedirect(request.POST['url']) + else: + _update_type(request.user, incident_ids, event_type) else: id = request.POST.get('id') - _update_type(request.user, [id], event_type) + incident = Incident.objects.get(id=id) + if incident.event_type == 'resolve' and event_type == 'acknowledge': + messages.error(request, 'Can\' ACK a resolved incident!') + return HttpResponseRedirect(request.POST['url']) + else: + _update_type(request.user, [id], event_type) except Incident.DoesNotExist: messages.error(request, 'Incident not found') return HttpResponseRedirect(request.POST['url']) From db2c490b6107fcc17dd7baccfb52ec89fb5ed22d Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Fri, 12 May 2017 15:45:39 +0200 Subject: [PATCH 18/22] fix extra space in openduty/models.py --- openduty/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openduty/models.py b/openduty/models.py index e9930fc..1e1bf35 100644 --- a/openduty/models.py +++ b/openduty/models.py @@ -175,7 +175,7 @@ def __str__(self): def natural_key(self): return (self.service_key, self.incident_key) def clean(self): - if self.event_type not in ['trigger', 'acknowledge', 'unacknowledge', 'resolve']: + if self.event_type not in ['trigger', 'acknowledge','unacknowledge', 'resolve']: raise ValidationError("'%s' is an invalid event type, valid values are 'trigger', 'acknowledge', 'unacknowledge' and 'resolve'" % self.event_type) @python_2_unicode_compatible From 3dd4786c82b550356dbec5df303156527bf615f7 Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Fri, 12 May 2017 17:40:19 +0200 Subject: [PATCH 19/22] add the option to notify groups of people in escalation rules, events; add pseudo-management of users in groups with checkboxes on user edit page(django admin still needed to create groups) --- notification/helper.py | 38 ++++++++++---- openduty/escalation.py | 24 +++++++-- openduty/escalation_helper.py | 54 ++++++++++++++++---- openduty/events.py | 8 ++- openduty/models.py | 11 ++-- openduty/serializers.py | 2 +- openduty/templates/escalation/edit.html | 22 +++++--- openduty/templates/event/edit.html | 67 ++++++++++++++++++++----- openduty/templates/users/edit.html | 16 +++++- openduty/users.py | 28 +++++++++-- 10 files changed, 217 insertions(+), 53 deletions(-) diff --git a/notification/helper.py b/notification/helper.py index 0470eab..8c6e5ef 100644 --- a/notification/helper.py +++ b/notification/helper.py @@ -1,8 +1,10 @@ __author__ = 'deathowl' from datetime import datetime, timedelta +from django.db import transaction from notification.tasks import send_notifications from openduty.escalation_helper import get_escalation_for_service +from openduty.models import Incident from django.utils import timezone from notification.models import ScheduledNotification from django.conf import settings @@ -27,24 +29,41 @@ def notify_user_about_incident(incident, user, delay=None, preparedmsg = None): @staticmethod def generate_notifications_for_incident(incident): now = timezone.make_aware(datetime.now(), timezone.get_current_timezone()) - duty_officers = get_escalation_for_service(incident.service_key) - + duty_officers = [] + # if incident has been escalated, notify according to the escalated service's escalation rule + if hasattr(incident, 'service_to_escalate_to') and incident.service_to_escalate_to is not None: + print "escalation rule in place to " + incident.service_to_escalate_to.name + duty_officers = get_escalation_for_service(incident.service_to_escalate_to) + with transaction.atomic(): + incident.description = "[escalated] " + incident.description + incident.save() + else: + duty_officers = get_escalation_for_service(incident.service_key) current_time = now notifications = [] + group_index = {} + user_method_index = {} for officer_index, duty_officer in enumerate(duty_officers): if incident.event_type == Incident.RESOLVE and not duty_officer.profile.send_resolve_enabled: print "Skipping notification for %s because type is RESOLVE and user %s has send_resolve_enabled OFF" % (incident.incident_key, duty_officer.username) continue - escalation_time = incident.service_key.escalate_after * (officer_index + 1) + index = 0 + if hasattr(duty_officer ,'came_from_group' ): + if not duty_officer.came_from_group in group_index: + group_index[duty_officer.came_from_group] = officer_index + index = group_index[duty_officer.came_from_group] + else: + index = officer_index + escalation_time = incident.service_key.escalate_after * (index + 1) escalate_at = current_time + timedelta(minutes=escalation_time) + user_method_index[duty_officer.username] = 0 methods = duty_officer.notification_methods.order_by('position').all() - method_index = 0 for method in methods: - notification_time = incident.service_key.retry * method_index + incident.service_key.escalate_after * officer_index + notification_time = incident.service_key.retry * user_method_index[duty_officer.username] + incident.service_key.escalate_after * index notify_at = current_time + timedelta(minutes=notification_time) if notify_at < escalate_at: notification = ScheduledNotification() @@ -53,14 +72,14 @@ def generate_notifications_for_incident(incident): notification.notifier = method.method notification.send_at = notify_at uri = settings.BASE_URL + "/incidents/details/" + str(incident.id) - notification.message = "A Service is experiencing a problem: " + incident.incident_key + " " + incident.description + ". Handle at: " + uri + " Details: " + incident.details + notification.message = incident.description + ". Handle at: " + uri + " Details: " + incident.details notifications.append(notification) - print "Notify %s at %s with method: %s" % (duty_officer.username, notify_at, notification.notifier) + print "[%s] Notify %s about %s at %s with method: %s" % (notification.incident.event_type, duty_officer.username, notification.incident.incident_key, notify_at, notification.notifier) else: break - method_index += 1 + user_method_index[duty_officer.username] += 1 # todo: error handling @@ -92,7 +111,8 @@ def generate_notifications_for_user(incident, user, delay=None, preparedmsg = No else: notification.message = preparedmsg notifications.append(notification) - print "Notify %s at %s with method: %s" % (user.username, notify_at, notification.notifier) + if notification.incident: + print "[%s] Notify %s at %s with method: %s" % (notification.incident.event_type, user.username, notify_at, notification.notifier) method_index += 1 # todo: error handling diff --git a/openduty/escalation.py b/openduty/escalation.py index 4f14017..f685adb 100644 --- a/openduty/escalation.py +++ b/openduty/escalation.py @@ -5,6 +5,7 @@ from django.contrib.auth.decorators import login_required from .models import Calendar, User, SchedulePolicy, SchedulePolicyRule +from django.contrib.auth.models import Group from django.http import Http404 from django.views.decorators.http import require_http_methods from django.db import IntegrityError @@ -32,12 +33,16 @@ def new(request): users = User.objects.all() except User.DoesNotExist: users = None + try: + groups = Group.objects.all() + except Group.DoesNotExist: + groups = None try: calendars = Calendar.objects.all() except Calendar.DoesNotExist: calendars = None - return TemplateResponse(request, 'escalation/edit.html', {'calendars': calendars, 'users': users}) + return TemplateResponse(request, 'escalation/edit.html', {'calendars': calendars, 'groups': groups, 'users': users}) @login_required() def edit(request, id): @@ -51,13 +56,17 @@ def edit(request, id): calendars = Calendar.objects.all() except Calendar.DoesNotExist: calendars = None + try: + groups = Group.objects.all() + except Group.DoesNotExist: + groups = None try: users = User.objects.all() except User.DoesNotExist: users = None return TemplateResponse(request, 'escalation/edit.html', {'item': policy, 'elements': elements, - 'calendars': calendars, 'users': users}) + 'calendars': calendars, 'groups': groups, 'users': users}) except Calendar.DoesNotExist: raise Http404 @@ -91,12 +100,16 @@ def save(request): parts = item.split("|") rule.escalate_after = 0 # HACK! rule.position = idx + 1 + rule.schedule = None + rule.user_id = None + rule.group_id = None if parts[0] == "user": rule.user_id = User.objects.get(id=parts[1]) - rule.schedule = None - if parts[0] == "calendar": + elif parts[0] == "calendar": rule.schedule = Calendar.objects.get(id=parts[1]) - rule.user_id = None + elif parts[0] == "group": + rule.group_id = Group.objects.get(id=parts[1]) + try: rule.save() except IntegrityError: @@ -104,3 +117,4 @@ def save(request): return HttpResponseRedirect('/policies/') + diff --git a/openduty/escalation_helper.py b/openduty/escalation_helper.py index 4873bef..fb20b42 100644 --- a/openduty/escalation_helper.py +++ b/openduty/escalation_helper.py @@ -1,22 +1,45 @@ __author__ = 'deathowl' from .models import User, SchedulePolicyRule, Service +from django.contrib.auth.models import Group from datetime import datetime, timedelta from django.utils import timezone from schedule.periods import Day from datetime import timedelta -def get_current_events_users(calendar): + +"""def get_current_events_users(calendar): now = timezone.make_aware(datetime.now(), timezone.get_current_timezone()) result = [] day = Day(calendar.events.all(), now) for o in day.get_occurrences(): if o.start <= now <= o.end: usernames = o.event.title.split(',') + print usernames for username in usernames: result.append(User.objects.get(username=username.strip())) return result +""" +def get_current_events_users(calendar): + now = timezone.make_aware(datetime.now(), timezone.get_current_timezone()) + result = [] + day = Day(calendar.events.all(), now) + for o in day.get_occurrences(): + if o.start <= now <= o.end: + items = o.event.title.split(',') + for item in items: + if Group.objects.filter(name=item.strip()).exists(): + for user in User.objects.filter(groups__name=item.strip()): + user.came_from_group = item.strip() + result.append(user) + else: + result.append(User.objects.get(username=item.strip())) + #tache suivante apres add group calendar + return result + + + def get_events_users_inbetween(calendar, since, until): delta = until - since result = {} @@ -26,14 +49,22 @@ def get_events_users_inbetween(calendar, since, until): day = Day(calendar.events.all(), that_day) for o in day.get_occurrences(): if o.start <= that_day <= o.end: - usernames = o.event.title.split(',') - for username in usernames: - if username not in result.keys(): - user_instance = User.objects.get(username=username.strip()) - result[username] = {"start": o.start, "person": username.strip(), "end": o.end, - "email": user_instance.email} + items = o.event.title.split(',') + for item in items: + username = item + if Group.objects.filter(name=item.strip()) is not None: + for user in User.objects.filter(groups__name=item): + if user not in result.keys(): + result.append(user) + else: + result[username]["end"] = o.end else: - result[username]["end"] = o.end + if item not in result.keys(): + user_instance = User.objects.get(username=item.strip()) + result[username] = {"start": o.start, "person": username.strip(), "end": o.end, + "email": user_instance.email} + else: + result[username]["end"] = o.end return result.values() @@ -45,8 +76,12 @@ def get_escalation_for_service(service): for item in rules: if item.schedule: result += get_current_events_users(item.schedule) - if item.user_id: + elif item.user_id: result.append(item.user_id) + elif item.group_id and Group.objects.filter(name=item.group_id) is not None: + for user in User.objects.filter(groups__name=item.group_id): + user.came_from_group = item.group_id.name + result.append(user) #TODO: This isnt de-deuped, is that right? return result @@ -56,3 +91,4 @@ def services_where_user_is_on_call(user): Q(policy__rules__user_id=user) | Q(policy__rules__schedule__event__title__icontains=user) ) return services + diff --git a/openduty/events.py b/openduty/events.py index baa5300..75688c7 100644 --- a/openduty/events.py +++ b/openduty/events.py @@ -1,4 +1,5 @@ import datetime +from django.contrib.auth.models import Group from django.core.urlresolvers import reverse from django.http import Http404, HttpResponseRedirect from django.shortcuts import get_object_or_404, render_to_response @@ -38,7 +39,9 @@ def create_or_edit_event(request, calendar_slug, event_id=None, next=None, if data: data["title"] = data["oncall"]+","+data["fallback"] form = form_class(data=data or None, instance=instance, initial=initial_data) - users = User.objects.all(); + users = User.objects.all() + groups = Group.objects.all() + #users = Item.groups.all(); if form.is_valid(): event = form.save(commit=False) if instance is None: @@ -57,7 +60,7 @@ def create_or_edit_event(request, calendar_slug, event_id=None, next=None, if instance.end_recurring_period: data["recurr_ymd"] = instance.end_recurring_period.date().isoformat() data["description"] = instance.description - data["rule"] = instance.rule and instance.rule.id or "" + data["rule"] = (instance.rule and instance.rule.id) or "" next = get_next_url(request, next) @@ -66,6 +69,7 @@ def create_or_edit_event(request, calendar_slug, event_id=None, next=None, "calendar": calendar, "next":next, "users":users, + "groups": groups, "form": form, }, context_instance=RequestContext(request)) diff --git a/openduty/models.py b/openduty/models.py index 1e1bf35..4201222 100644 --- a/openduty/models.py +++ b/openduty/models.py @@ -7,7 +7,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import python_2_unicode_compatible -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group from uuidfield import UUIDField from django.core.exceptions import ValidationError from schedule.models import Calendar @@ -74,7 +74,6 @@ class Service(models.Model): notifications_disabled = models.BooleanField(default=False) send_resolve_enabled = models.BooleanField(default=False) - class Meta: verbose_name = _('service') verbose_name_plural = _('service') @@ -113,6 +112,7 @@ def color(self): 'silence_incident': 'active', 'unsilence_incident': 'active', 'forward': 'info', + 'escalate': 'info', 'trigger': 'trigger', 'notified': 'success', 'notification_failed': 'danger', @@ -142,15 +142,17 @@ class Incident(models.Model): RESOLVE = "resolve" ACKNOWLEDGE = "acknowledge" UNACKNOWLEDGE = "unacknowledge" + ESCALATE = "escalate" """ Incidents are representations of a malfunction in the system. """ - service_key = models.ForeignKey(Service) + service_key = models.ForeignKey(Service,related_name="incident") incident_key = models.CharField(max_length=200) event_type = models.CharField(max_length=15) description = models.CharField(max_length=200) details = models.TextField() occurred_at = models.DateTimeField() + service_to_escalate_to = models.ForeignKey(Service,related_name="service_to_escalate_to_id",null=True, blank=True, default = None) @property def color(self): @@ -205,6 +207,7 @@ class SchedulePolicyRule(models.Model): schedule_policy = models.ForeignKey(SchedulePolicy, related_name='rules') position = models.IntegerField() user_id = models.ForeignKey(User, blank=True, null=True) + group_id = models.ForeignKey(Group, blank=True, null=True) schedule = models.ForeignKey(Calendar, blank=True, null=True) escalate_after = models.IntegerField() @@ -217,7 +220,7 @@ def __str__(self): @classmethod def getRulesForService(cls, service): - return cls.objects.filter(schedule_policy=service.policy) + return cls.objects.filter(schedule_policy=service.policy.id) class UserProfile(models.Model): user = models.OneToOneField('auth.User', related_name='profile') diff --git a/openduty/serializers.py b/openduty/serializers.py index d12cedf..bad826a 100644 --- a/openduty/serializers.py +++ b/openduty/serializers.py @@ -37,7 +37,7 @@ class SchedulePolicyRuleSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = SchedulePolicyRule - fields = ('schedule_policy', 'position', 'user_id', 'schedule', 'escalate_after') + fields = ('schedule_policy', 'position', 'user_id', 'group_id', 'schedule', 'escalate_after') class NoneSerializer(serializers.Serializer): diff --git a/openduty/templates/escalation/edit.html b/openduty/templates/escalation/edit.html index babcf5d..017edb9 100644 --- a/openduty/templates/escalation/edit.html +++ b/openduty/templates/escalation/edit.html @@ -14,13 +14,17 @@ $('#add').on('click', function () { list.append( '
  • ' + - ''+ - '
    '+ + ''+ + '
    '+ ' {% for item in calendars %} {% endfor %} + + {% for item in groups %} + + {% endfor %} + + {% for item in users %} diff --git a/openduty/templates/event/edit.html b/openduty/templates/event/edit.html index fe4a47a..8808eec 100644 --- a/openduty/templates/event/edit.html +++ b/openduty/templates/event/edit.html @@ -39,7 +39,7 @@ }); }); -{% trans "Create or change event. All fields are required." %} +{% trans "Create or change an event. All fields are required." %}

    {% csrf_token %} @@ -58,23 +58,67 @@ The end time must be later than start time.
    +
    -
    +
    + + + + + +
    + {% for group in grouped_models %} +
    +

    {{ group.grouper.verbose_name }} ({{ group.grouper.name }})

    + + {% for model in group.list %} + + + + {% endfor %} +
    {{ model.object_name }}
    +
    + {% endfor %} +
    + + + + + + +
    @@ -95,12 +139,9 @@ class="datepicker form-control" value="{% if data.recurr_ymd %} {{data.recurr_ymd}}{% endif %}">
  • -
    - +
    - +

    diff --git a/openduty/templates/users/edit.html b/openduty/templates/users/edit.html index 5dd5a97..6cfb294 100644 --- a/openduty/templates/users/edit.html +++ b/openduty/templates/users/edit.html @@ -60,7 +60,21 @@
    - + +
    +
    +
    + +
    + {% if all_groups %} + {% for group in all_groups %} +
    + +
    + {% endfor %} + {% endif %}
    diff --git a/openduty/users.py b/openduty/users.py index daa3b14..9f6ff91 100644 --- a/openduty/users.py +++ b/openduty/users.py @@ -8,6 +8,7 @@ from django.template.response import TemplateResponse from django.contrib.auth.decorators import login_required from .models import User, UserProfile +from django.contrib.auth.models import Group from django.http import Http404 from django.views.decorators.http import require_http_methods from django.db import IntegrityError @@ -16,6 +17,7 @@ from django.conf import settings from django.contrib.admin.views.decorators import staff_member_required from django.core.exceptions import PermissionDenied +import itertools @login_required() @@ -40,10 +42,11 @@ def edit(request, id): try: user = User.objects.get(id = id) user_methods = UserNotificationMethod.objects.filter(user = user).order_by('position') - + all_groups = Group.objects.all() + user_groups = [str(x.name) for x in User.objects.get(id=id).groups.all()] return TemplateResponse( request, 'users/edit.html', - {'item': user, 'methods': UserNotificationMethod.methods, 'user_methods': user_methods, 'empty_user_method': UserNotificationMethod(), 'hipchat_rooms': HipchatNotifier(settings.HIPCHAT_SETTINGS).get_all_rooms()} + {'item': user, 'all_groups': all_groups, 'user_groups': user_groups, 'methods': UserNotificationMethod.methods, 'user_methods': user_methods, 'empty_user_method': UserNotificationMethod(), 'hipchat_rooms': HipchatNotifier(settings.HIPCHAT_SETTINGS).get_all_rooms()} ) except User.DoesNotExist: raise Http404 @@ -51,7 +54,7 @@ def edit(request, id): @login_required() @staff_member_required def new(request): - return TemplateResponse(request, 'users/edit.html', {'methods': UserNotificationMethod.methods, 'empty_user_method': UserNotificationMethod(), 'hipchat_rooms': HipchatNotifier(settings.HIPCHAT_SETTINGS).get_all_rooms()}) + return TemplateResponse(request, 'users/edit.html', {'methods': UserNotificationMethod.methods, 'empty_user_method': UserNotificationMethod(), 'hipchat_rooms': HipchatNotifier (settings.HIPCHAT_SETTINGS).get_all_rooms()}) @login_required() @require_http_methods(["POST"]) @@ -70,6 +73,25 @@ def save(request): user.set_password(request.POST['password']) try: + user.save() + all_groups = [] + for group in Group.objects.all(): + all_groups.append(int(group.id)) + post_groups = request.POST.getlist('groups[]') + for idx, group in enumerate(post_groups): + group = int(group) + if group in all_groups: + all_groups.remove(group) + if group not in [x.id for x in User.objects.get(id=request.POST['id']).groups.all()]: #Groups.objects.filter(id__in=user.groups.all().values_list('id', flat=True))]: + try: + user.groups.add(group) + except e: + messages.error(request, str(e)) + + if len(all_groups) > 0: + for group in all_groups: + user.groups.remove(group) + user.save() try: UserNotificationMethod.objects.filter(user=user).delete() From 77f13822333c7ac34725432be5d31b6b7e115660 Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Fri, 12 May 2017 17:44:24 +0200 Subject: [PATCH 20/22] add the option to search by incident_key with autosuggest for the latest 500 incidents --- openduty/tables.py | 4 +++- openduty/templates/incidents/incident_filter.html | 14 ++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/openduty/tables.py b/openduty/tables.py index af48ee8..3726040 100644 --- a/openduty/tables.py +++ b/openduty/tables.py @@ -35,7 +35,9 @@ class IncidentTable(tables.Table): values_list=[ (str(x), str(x.id)) for x in Service.objects.all()]), F('event_type', 'Event', values_list=EventLog.ACTIONS), - ) + F('incident_key', 'Incident Key', + values_list= [(i, i) for i in Incident.objects.values_list('incident_key', flat=True).order_by('-occurred_at')[:500] ]) + ) tr_class = tables.Column(visible=False, empty_values=()) def render_tr_class(self, record): diff --git a/openduty/templates/incidents/incident_filter.html b/openduty/templates/incidents/incident_filter.html index 3b023b7..1328a57 100644 --- a/openduty/templates/incidents/incident_filter.html +++ b/openduty/templates/incidents/incident_filter.html @@ -8,21 +8,23 @@

    Filter

    - + {% for obj in f.values_list %} {% endfor %} - + +
    {% endfor %}
    - - Reset + + Reset
    -
    \ No newline at end of file +
    +

    From 1644473f33339d4a5966c0a2cd8c91e2dd974b6d Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Fri, 12 May 2017 17:54:04 +0200 Subject: [PATCH 21/22] add example systemd service files --- README.md | 10 ++++++---- systemd/celery.service | 14 ++++++++++++++ systemd/celery.service.d/main.conf | 7 +++++++ systemd/flower.service | 14 ++++++++++++++ systemd/flower.service.d/main.conf | 7 +++++++ systemd/gunicorn.service | 20 ++++++++++++++++++++ systemd/gunicorn.service.d/main.conf | 8 ++++++++ 7 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 systemd/celery.service create mode 100644 systemd/celery.service.d/main.conf create mode 100644 systemd/flower.service create mode 100644 systemd/flower.service.d/main.conf create mode 100644 systemd/gunicorn.service create mode 100644 systemd/gunicorn.service.d/main.conf diff --git a/README.md b/README.md index 688884f..f7bff49 100644 --- a/README.md +++ b/README.md @@ -82,11 +82,13 @@ now, you can start hacking on it. # Running as a service with systemd *OpenDuty can be ran as a service with the help of gunicorn and systemd* ``` -cp systemd/gunicorn.service /etc/systemd/system/ -cp -r systemd/gunicorn.service.d /etc/systemd/system/gunicorn.service.d +cp -r systemd/gunicorn.service.* /etc/systemd/system/ -cp systemd/celery.service /etc/systemd/system/ -cp -r systemd/celery.service.d /etc/systemd/system/ +cp -r systemd/celery.service* /etc/systemd/system/ + +// EDIT VARIABLES IN *.service.d/main.conf TO REFLECT YOUR ENV +vi /etc/systemd/system/gunicorn.service.d/main.conf +vi /etc/systemd/system/celery.service.d/main.conf systemctl daemon-reload sudo systemctl start gunicorn diff --git a/systemd/celery.service b/systemd/celery.service new file mode 100644 index 0000000..627b74b --- /dev/null +++ b/systemd/celery.service @@ -0,0 +1,14 @@ +[Unit] +Description=Celery daemon +After=network.target gunicorn.service + +[Service] +ExecStart=/bin/sh -c "/$WorkingDirectory/env/bin/celery worker --uid=$User --gid=$Group --logfile=$LogFile --loglevel=info --pidfile=$PIDFile --autoscale=2,1 --app=openduty --workdir=/$WorkingDirectory" +ExecReload=/bin/kill -s HUP $MAINPID +ExecStop=/bin/kill -s TERM $MAINPID +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target + diff --git a/systemd/celery.service.d/main.conf b/systemd/celery.service.d/main.conf new file mode 100644 index 0000000..a8a4db2 --- /dev/null +++ b/systemd/celery.service.d/main.conf @@ -0,0 +1,7 @@ +[Service] +Environment="User=www-data" +Environment="Group=www-data" +Environment="PIDFile=/run/openduty/celery.pid" +Environment="WorkingDirectory=usr/local/bin/openduty" +Environment="LogFile=/var/log/openduty/celery.log" + diff --git a/systemd/flower.service b/systemd/flower.service new file mode 100644 index 0000000..6b809d9 --- /dev/null +++ b/systemd/flower.service @@ -0,0 +1,14 @@ +[Unit] +Description=Flower Service +After=network.target celery.service + +[Service] +ExecStart=/bin/sh -c "/$WorkingDirectory/env/bin/python /$WorkingDirectory/env/bin/flower --app=openduty --port=5555 --logfile=$LogFile --loglevel=info --pidfile=$PIDFile --workdir=/$WorkingDirectory --url_prefix=flower" +ExecReload=/bin/kill -s HUP $MAINPID +ExecStop=/bin/kill -s TERM $MAINPID +Restart=always +RestartSec=3 + + +[Install] +WantedBy=multi-user.target diff --git a/systemd/flower.service.d/main.conf b/systemd/flower.service.d/main.conf new file mode 100644 index 0000000..df3b018 --- /dev/null +++ b/systemd/flower.service.d/main.conf @@ -0,0 +1,7 @@ +[Service] +Environment="User=www-data" +Environment="Group=www-data" +Environment="PIDFile=/run/openduty/flower.pid" +Environment="WorkingDirectory=usr/local/bin/openduty" +Environment="LogFile=/var/log/openduty/flower.log" +Environment="ErrorFiles=/var/log/openduty/flower.error.log" diff --git a/systemd/gunicorn.service b/systemd/gunicorn.service new file mode 100644 index 0000000..cf7e3dd --- /dev/null +++ b/systemd/gunicorn.service @@ -0,0 +1,20 @@ +[Unit] +Description=gunicorn daemon +After=network.target + +[Service] +PermissionsStartOnly=true +User=www-data +Group=www-data +ExecStartPre=/bin/mkdir -p /var/run/openduty/ +ExecStartPre=/bin/chown -R www-data:www-data /var/run/openduty +ExecStart=/bin/sh -c "/$WorkingDirectory/env/bin/gunicorn --user=$User --workers=9 --pid=$PIDFile --log-file=$LogFile --log-level=Debug --capture-output --error-logfile=$ErrorLogFile --chdir=$WorkingDirectory --bind 0.0.0.0:8080 openduty.wsgi:application" +ExecReload=/bin/kill -s HUP $MAINPID +ExecStop=/bin/kill -s TERM $MAINPID +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target + + diff --git a/systemd/gunicorn.service.d/main.conf b/systemd/gunicorn.service.d/main.conf new file mode 100644 index 0000000..de5b65e --- /dev/null +++ b/systemd/gunicorn.service.d/main.conf @@ -0,0 +1,8 @@ +[Service] +Environment="User=www-data" +Environment="Group=www-data" +Environment="PIDFile=/run/openduty/gunicorn.pid" +Environment="WorkingDirectory=usr/local/bin/openduty" +Environment="LogFile=/var/log/openduty/gunicorn.log" +Environment="ErrorLogFile=/var/log/openduty/gunicorn.error.log" + From 5dddc86521301b8c7c2070a882c9b6a7870dbef9 Mon Sep 17 00:00:00 2001 From: Adrian Todorov Date: Fri, 12 May 2017 17:54:53 +0200 Subject: [PATCH 22/22] add example nginx conf for reverse rpoxy to gunicorn --- nginx.conf | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 nginx.conf diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..560c2b8 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,15 @@ +server { + listen 80; + server_name _; + access_log /var/log/openduty/nginx/access.log; + error_log /var/log/openduty/nginx/error.log; + location / { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://localhost:8080; + } +} + +