# coding: utf-8
|
|
|
|
import hashlib
|
|
import json
|
|
import uuid
|
|
from datetime import timedelta as td
|
|
|
|
import requests
|
|
from django.conf import settings
|
|
from django.contrib.auth.models import User
|
|
from django.core.urlresolvers import reverse
|
|
from django.db import models
|
|
from django.template.loader import render_to_string
|
|
from django.utils import timezone
|
|
from hc.lib import emails
|
|
|
|
STATUSES = (
|
|
("up", "Up"),
|
|
("down", "Down"),
|
|
("new", "New"),
|
|
("paused", "Paused")
|
|
)
|
|
DEFAULT_TIMEOUT = td(days=1)
|
|
DEFAULT_GRACE = td(hours=1)
|
|
CHANNEL_KINDS = (("email", "Email"), ("webhook", "Webhook"),
|
|
("hipchat", "HipChat"),
|
|
("slack", "Slack"), ("pd", "PagerDuty"), ("po", "Pushover"))
|
|
|
|
PO_PRIORITIES = {
|
|
-2: "lowest",
|
|
-1: "low",
|
|
0: "normal",
|
|
1: "high",
|
|
2: "emergency"
|
|
}
|
|
|
|
|
|
class Check(models.Model):
|
|
|
|
class Meta:
|
|
# sendalerts command will query using these
|
|
index_together = ["status", "user", "alert_after"]
|
|
|
|
name = models.CharField(max_length=100, blank=True)
|
|
code = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
|
user = models.ForeignKey(User, blank=True, null=True)
|
|
created = models.DateTimeField(auto_now_add=True)
|
|
timeout = models.DurationField(default=DEFAULT_TIMEOUT)
|
|
grace = models.DurationField(default=DEFAULT_GRACE)
|
|
last_ping = models.DateTimeField(null=True, blank=True)
|
|
alert_after = models.DateTimeField(null=True, blank=True, editable=False)
|
|
status = models.CharField(max_length=6, choices=STATUSES, default="new")
|
|
|
|
def name_then_code(self):
|
|
if self.name:
|
|
return self.name
|
|
|
|
return str(self.code)
|
|
|
|
def url(self):
|
|
return settings.PING_ENDPOINT + str(self.code)
|
|
|
|
def email(self):
|
|
return "%s@%s" % (self.code, settings.PING_EMAIL_DOMAIN)
|
|
|
|
def send_alert(self):
|
|
if self.status not in ("up", "down"):
|
|
raise NotImplemented("Unexpected status: %s" % self.status)
|
|
|
|
for channel in self.channel_set.all():
|
|
channel.notify(self)
|
|
|
|
def get_status(self):
|
|
if self.status in ("new", "paused"):
|
|
return self.status
|
|
|
|
now = timezone.now()
|
|
|
|
if self.last_ping + self.timeout > now:
|
|
return "up"
|
|
|
|
if self.last_ping + self.timeout + self.grace > now:
|
|
return "grace"
|
|
|
|
return "down"
|
|
|
|
def assign_all_channels(self):
|
|
if self.user:
|
|
channels = Channel.objects.filter(user=self.user)
|
|
self.channel_set.add(*channels)
|
|
|
|
|
|
class Ping(models.Model):
|
|
owner = models.ForeignKey(Check)
|
|
created = models.DateTimeField(auto_now_add=True)
|
|
scheme = models.CharField(max_length=10, default="http")
|
|
remote_addr = models.GenericIPAddressField(blank=True, null=True)
|
|
method = models.CharField(max_length=10, blank=True)
|
|
ua = models.CharField(max_length=200, blank=True)
|
|
body = models.TextField(blank=True)
|
|
|
|
|
|
class Channel(models.Model):
|
|
code = models.UUIDField(default=uuid.uuid4, editable=False)
|
|
user = models.ForeignKey(User)
|
|
created = models.DateTimeField(auto_now_add=True)
|
|
kind = models.CharField(max_length=20, choices=CHANNEL_KINDS)
|
|
value = models.CharField(max_length=200, blank=True)
|
|
email_verified = models.BooleanField(default=False)
|
|
checks = models.ManyToManyField(Check)
|
|
|
|
def make_token(self):
|
|
seed = "%s%s" % (self.code, settings.SECRET_KEY)
|
|
seed = seed.encode("utf8")
|
|
return hashlib.sha1(seed).hexdigest()
|
|
|
|
def send_verify_link(self):
|
|
args = [self.code, self.make_token()]
|
|
verify_link = reverse("hc-verify-email", args=args)
|
|
verify_link = settings.SITE_ROOT + verify_link
|
|
emails.verify_email(self.value, {"verify_link": verify_link})
|
|
|
|
def notify(self, check):
|
|
n = Notification(owner=check, channel=self)
|
|
n.check_status = check.status
|
|
|
|
if self.kind == "email" and self.email_verified:
|
|
ctx = {
|
|
"check": check,
|
|
"checks": self.user.check_set.order_by("created"),
|
|
"now": timezone.now()
|
|
}
|
|
emails.alert(self.value, ctx)
|
|
n.save()
|
|
elif self.kind == "webhook" and check.status == "down":
|
|
try:
|
|
r = requests.get(self.value, timeout=5)
|
|
n.status = r.status_code
|
|
except requests.exceptions.Timeout:
|
|
# Well, we tried
|
|
pass
|
|
|
|
n.save()
|
|
elif self.kind == "slack":
|
|
tmpl = "integrations/slack_message.html"
|
|
text = render_to_string(tmpl, {"check": check})
|
|
payload = {
|
|
"text": text,
|
|
"username": "healthchecks.io",
|
|
"icon_url": "https://healthchecks.io/static/img/[email protected]"
|
|
}
|
|
|
|
r = requests.post(self.value, json=payload, timeout=5)
|
|
|
|
n.status = r.status_code
|
|
n.save()
|
|
elif self.kind == "hipchat":
|
|
tmpl = "integrations/hipchat_message.html"
|
|
text = render_to_string(tmpl, {"check": check})
|
|
payload = {
|
|
"message": text,
|
|
"color": "green" if check.status == "up" else "red",
|
|
}
|
|
|
|
r = requests.post(self.value, json=payload, timeout=5)
|
|
|
|
n.status = r.status_code
|
|
n.save()
|
|
|
|
elif self.kind == "pd":
|
|
if check.status == "down":
|
|
event_type = "trigger"
|
|
description = "%s is DOWN" % check.name_then_code()
|
|
else:
|
|
event_type = "resolve"
|
|
description = "%s received a ping and is now UP" % \
|
|
check.name_then_code()
|
|
|
|
payload = {
|
|
"service_key": self.value,
|
|
"incident_key": str(check.code),
|
|
"event_type": event_type,
|
|
"description": description,
|
|
"client": "healthchecks.io",
|
|
"client_url": settings.SITE_ROOT
|
|
}
|
|
|
|
url = "https://events.pagerduty.com/generic/2010-04-15/create_event.json"
|
|
r = requests.post(url, data=json.dumps(payload), timeout=5)
|
|
|
|
n.status = r.status_code
|
|
n.save()
|
|
|
|
elif self.kind == "po":
|
|
tmpl = "integrations/pushover_message.html"
|
|
ctx = {
|
|
"check": check,
|
|
"down_checks": self.user.check_set.filter(status="down").exclude(code=check.code).order_by("created"),
|
|
}
|
|
text = render_to_string(tmpl, ctx).strip()
|
|
if check.status == "down":
|
|
title = "%s is DOWN" % check.name_then_code()
|
|
else:
|
|
title = "%s is now UP" % check.name_then_code()
|
|
|
|
user_key, priority, _ = self.po_value
|
|
payload = {
|
|
"token": settings.PUSHOVER_API_TOKEN,
|
|
"user": user_key,
|
|
"message": text,
|
|
"title": title,
|
|
"html": 1,
|
|
"priority": priority,
|
|
}
|
|
if priority == 2: # Emergency notification
|
|
payload["retry"] = settings.PUSHOVER_EMERGENCY_RETRY_DELAY
|
|
payload["expire"] = settings.PUSHOVER_EMERGENCY_EXPIRATION
|
|
|
|
url = "https://api.pushover.net/1/messages.json"
|
|
r = requests.post(url, data=payload, timeout=5)
|
|
|
|
n.status = r.status_code
|
|
n.save()
|
|
|
|
@property
|
|
def po_value(self):
|
|
assert self.kind == "po"
|
|
user_key, prio = self.value.split("|")
|
|
prio = int(prio)
|
|
return user_key, prio, PO_PRIORITIES[prio]
|
|
|
|
|
|
class Notification(models.Model):
|
|
owner = models.ForeignKey(Check)
|
|
check_status = models.CharField(max_length=6)
|
|
channel = models.ForeignKey(Channel)
|
|
created = models.DateTimeField(auto_now_add=True)
|
|
status = models.IntegerField(default=0)
|