Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: b37573aba3
Fetching contributors…

Cannot retrieve contributors at this time

317 lines (253 sloc) 11.376 kb
"""
overseer.models
~~~~~~~~~~~~~~~
A service's status should be:
- red if any(updates affecting service) are red
- yellow if any(updates affecting service) are yellow
- green if all(updates affecting service) are green
:copyright: (c) 2011 DISQUS.
:license: Apache License 2.0, see LICENSE for more details.
"""
import datetime
import oauth2
import urlparse
import uuid
import warnings
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models.signals import post_save, m2m_changed
from overseer import conf
from overseer.utils import SimpleTwitterClient
STATUS_CHOICES = (
(0, 'No Problems'),
(1, 'Some Issues'),
(2, 'Unavailable'),
)
SUBSCRIPTION_EMAIL_TEMPLATE = """
A service's status has changed on %(name)s:
%(message)s
This update affects the following:
%(affects)s
----
To change your subscription settings, please visit %(sub_url)s
""".strip()
class Service(models.Model):
"""
A ``Service`` can describe any part of your architecture. Each
service can have many events, in which the last event should be shown
(unless the status is 'No Problems').
"""
name = models.CharField(max_length=128)
slug = models.SlugField(max_length=128, unique=True)
description = models.TextField(blank=True, null=True)
status = models.SmallIntegerField(choices=STATUS_CHOICES, editable=False, default=0)
order = models.IntegerField(default=0)
date_created = models.DateTimeField(default=datetime.datetime.now, editable=False)
date_updated = models.DateTimeField(default=datetime.datetime.now, editable=False)
class Meta:
ordering = ('order', 'name')
def __unicode__(self):
return self.name
@models.permalink
def get_absolute_url(self):
return ('overseer:service', [self.slug], {})
@classmethod
def handle_event_m2m_save(cls, sender, instance, action, reverse, model, pk_set, **kwargs):
if not action.startswith('post_'):
return
if not pk_set:
return
if model is Service:
for service in Service.objects.filter(pk__in=pk_set):
service.update_from_event(instance)
else:
for event in Event.objects.filter(pk__in=pk_set):
instance.update_from_event(event)
@classmethod
def handle_event_save(cls, instance, **kwargs):
for service in instance.services.all():
service.update_from_event(instance)
def update_from_event(self, event):
update_qs = Service.objects.filter(pk=self.pk)
if event.date_updated > self.date_updated:
# If the update is newer than the last update to the self
update_qs.filter(date_updated__lt=event.date_updated)\
.update(date_updated=event.date_updated)
self.date_updated = event.date_updated
if event.status > self.status:
# If our status more critical (higher) than the current
# self status, update to match the current
update_qs.filter(status__lt=event.status)\
.update(status=event.status)
self.status = event.status
elif event.status < self.status:
# If no more events match the current self status, let's update
# it to the current status
if not Event.objects.filter(services=self, status=self.status)\
.exclude(pk=event.pk).exists():
update_qs.filter(status__gt=event.status)\
.update(status=event.status)
self.status = event.status
def get_message(self):
if self.status == 0:
return 'This service is operating as expected.'
elif self.status == 1:
return 'This service is experiencing some issues.'
elif self.status == 2:
return 'This service may be unavailable.'
return ''
def join_with_and(values):
values = list(values)
if len(values) == 2:
return ' and '.join(values)
elif len(values) > 2:
return '%s, and %s' % (', '.join(values[:-1]), values[-1])
return values[0]
class EventBase(models.Model):
class Meta:
abstract = True
def get_message(self):
if self.message:
return self.message
elif self.status == 0:
return '%s operating as expected.' % join_with_and(a[1] for a in self.get_services())
elif self.status == 1:
return 'Experiencing some issues with %s.' % join_with_and(a[1] for a in self.get_services())
elif self.status == 2:
return '%s may be unavailable.' % join_with_and(a[1] for a in self.get_services())
return ''
class Event(EventBase):
"""
An ``Event`` is a collection of updates related to one event.
- ``message`` stores the last message from ``StatusUpdate`` for this event.
"""
services = models.ManyToManyField(Service)
status = models.SmallIntegerField(choices=STATUS_CHOICES, editable=False, default=0)
peak_status = models.SmallIntegerField(choices=STATUS_CHOICES, editable=False, default=0)
description = models.TextField(null=True, blank=True, help_text='We will auto fill the description from the first event message if not set')
message = models.TextField(null=True, blank=True, editable=False)
date_created = models.DateTimeField(default=datetime.datetime.now, editable=False)
date_updated = models.DateTimeField(default=datetime.datetime.now, editable=False)
def __unicode__(self):
return u"%s on %s" % (self.date_created, '; '.join(self.services.values_list('name', flat=True)))
@models.permalink
def get_absolute_url(self):
return ('overseer:event', [self.pk], {})
def get_services(self):
return self.services.values_list('slug', 'name')
def get_duration(self):
return self.date_updated - self.date_created
def post_to_twitter(self, message=None):
"""Update twitter status, i.e., post a tweet"""
consumer = oauth2.Consumer(key=conf.TWITTER_CONSUMER_KEY,
secret=conf.TWITTER_CONSUMER_SECRET)
token = oauth2.Token(key=conf.TWITTER_ACCESS_TOKEN, secret=conf.TWITTER_ACCESS_SECRET)
client = SimpleTwitterClient(consumer=consumer, token=token)
if not message:
message = self.get_message()
hash_tag = '#status'
if conf.BASE_URL:
permalink = urlparse.urljoin(conf.BASE_URL, reverse('overseer:event_short', args=[self.pk]))
if len(message) + len(permalink) + len(hash_tag) > 138:
message = '%s.. %s %s' % (message[:140-4-len(hash_tag)-len(permalink)], permalink, hash_tag)
else:
message = '%s %s %s' % (message, permalink, hash_tag)
else:
if len(message) + len(hash_tag) > 139:
message = '%s.. %s' % (message[:140-3-len(hash_tag)], hash_tag)
else:
message = '%s %s' % (message, hash_tag)
return client.update_status(message)
@classmethod
def handle_update_save(cls, instance, created, **kwargs):
event = instance.event
if created:
is_latest = True
elif EventUpdate.objects.filter(event=event).order_by('-date_created')\
.values_list('event', flat=True)[0] == event.pk:
is_latest = True
else:
is_latest = False
if is_latest:
update_kwargs = dict(
status=instance.status,
date_updated=instance.date_created,
message=instance.message
)
if not event.description:
update_kwargs['description'] = instance.message
if not event.peak_status or event.peak_status < instance.status:
update_kwargs['peak_status'] = instance.status
Event.objects.filter(pk=event.pk).update(**update_kwargs)
for k, v in update_kwargs.iteritems():
setattr(event, k, v)
# Without sending the signal Service will fail to update
post_save.send(sender=Event, instance=event, created=False)
class EventUpdate(EventBase):
"""
An ``EventUpdate`` contains a single update to an ``Event``. The latest update
will always be reflected within the event, carrying over it's ``status`` and ``message``.
"""
event = models.ForeignKey(Event)
status = models.SmallIntegerField(choices=STATUS_CHOICES)
message = models.TextField(null=True, blank=True)
date_created = models.DateTimeField(default=datetime.datetime.now, editable=False)
def __unicode__(self):
return unicode(self.date_created)
def get_services(self):
return self.event.services.values_list('slug', 'name')
class BaseSubscription(models.Model):
ident = models.CharField(max_length=32, unique=True)
date_created = models.DateTimeField(default=datetime.datetime.now, editable=False)
services = models.ManyToManyField(Service)
class Meta:
abstract = True
def __unicode__(self):
return self.email
def save(self, *args, **kwargs):
if not self.ident:
self.ident = uuid.uuid4().hex
super(BaseSubscription, self).save(*args, **kwargs)
class Subscription(BaseSubscription):
"""
Represents an email subscription.
"""
email = models.EmailField(unique=True)
@classmethod
def handle_update_save(cls, instance, created, **kwargs):
if not created:
return
if not conf.ALLOW_SUBSCRIPTIONS:
return
if not conf.FROM_EMAIL:
# TODO: grab system default
warnings.warn('Configuration error with Oveerseer: FROM_EMAIL is not set')
return
if not conf.BASE_URL:
warnings.warn('Configuration error with Oveerseer: BASE_URL is not set')
return
services = list(instance.event.services.all())
affects = '\n'.join('- %s' % s.name for s in services)
message = instance.get_message()
for email, ident in cls.objects.filter(services__in=services)\
.values_list('email', 'ident')\
.distinct():
# send email
body = SUBSCRIPTION_EMAIL_TEMPLATE % dict(
sub_url = urlparse.urljoin(conf.BASE_URL, reverse('overseer:update_subscription', args=[ident])),
message = message,
affects = affects,
name = conf.NAME,
)
send_mail('Status Change on %s' % conf.NAME, body, conf.FROM_EMAIL, [email],
fail_silently=True)
class UnverifiedSubscription(BaseSubscription):
"""
A temporary store for unverified subscriptions.
"""
email = models.EmailField()
post_save.connect(Service.handle_event_save, sender=Event)
post_save.connect(Event.handle_update_save, sender=EventUpdate)
m2m_changed.connect(Service.handle_event_m2m_save, sender=Event.services.through)
post_save.connect(Subscription.handle_update_save, sender=EventUpdate)
Jump to Line
Something went wrong with that request. Please try again.