You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

237 lines
7.7 KiB

10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. # coding: utf-8
  2. import hashlib
  3. import json
  4. import uuid
  5. from datetime import timedelta as td
  6. import requests
  7. from django.conf import settings
  8. from django.contrib.auth.models import User
  9. from django.core.urlresolvers import reverse
  10. from django.db import models
  11. from django.template.loader import render_to_string
  12. from django.utils import timezone
  13. from hc.lib import emails
  14. STATUSES = (
  15. ("up", "Up"),
  16. ("down", "Down"),
  17. ("new", "New"),
  18. ("paused", "Paused")
  19. )
  20. DEFAULT_TIMEOUT = td(days=1)
  21. DEFAULT_GRACE = td(hours=1)
  22. CHANNEL_KINDS = (("email", "Email"), ("webhook", "Webhook"),
  23. ("hipchat", "HipChat"),
  24. ("slack", "Slack"), ("pd", "PagerDuty"), ("po", "Pushover"))
  25. PO_PRIORITIES = {
  26. -2: "lowest",
  27. -1: "low",
  28. 0: "normal",
  29. 1: "high",
  30. 2: "emergency"
  31. }
  32. class Check(models.Model):
  33. class Meta:
  34. # sendalerts command will query using these
  35. index_together = ["status", "user", "alert_after"]
  36. name = models.CharField(max_length=100, blank=True)
  37. code = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
  38. user = models.ForeignKey(User, blank=True, null=True)
  39. created = models.DateTimeField(auto_now_add=True)
  40. timeout = models.DurationField(default=DEFAULT_TIMEOUT)
  41. grace = models.DurationField(default=DEFAULT_GRACE)
  42. last_ping = models.DateTimeField(null=True, blank=True)
  43. alert_after = models.DateTimeField(null=True, blank=True, editable=False)
  44. status = models.CharField(max_length=6, choices=STATUSES, default="new")
  45. def name_then_code(self):
  46. if self.name:
  47. return self.name
  48. return str(self.code)
  49. def url(self):
  50. return settings.PING_ENDPOINT + str(self.code)
  51. def email(self):
  52. return "%s@%s" % (self.code, settings.PING_EMAIL_DOMAIN)
  53. def send_alert(self):
  54. if self.status not in ("up", "down"):
  55. raise NotImplemented("Unexpected status: %s" % self.status)
  56. for channel in self.channel_set.all():
  57. channel.notify(self)
  58. def get_status(self):
  59. if self.status in ("new", "paused"):
  60. return self.status
  61. now = timezone.now()
  62. if self.last_ping + self.timeout > now:
  63. return "up"
  64. if self.last_ping + self.timeout + self.grace > now:
  65. return "grace"
  66. return "down"
  67. def assign_all_channels(self):
  68. if self.user:
  69. channels = Channel.objects.filter(user=self.user)
  70. self.channel_set.add(*channels)
  71. class Ping(models.Model):
  72. owner = models.ForeignKey(Check)
  73. created = models.DateTimeField(auto_now_add=True)
  74. scheme = models.CharField(max_length=10, default="http")
  75. remote_addr = models.GenericIPAddressField(blank=True, null=True)
  76. method = models.CharField(max_length=10, blank=True)
  77. ua = models.CharField(max_length=200, blank=True)
  78. class Channel(models.Model):
  79. code = models.UUIDField(default=uuid.uuid4, editable=False)
  80. user = models.ForeignKey(User)
  81. created = models.DateTimeField(auto_now_add=True)
  82. kind = models.CharField(max_length=20, choices=CHANNEL_KINDS)
  83. value = models.CharField(max_length=200, blank=True)
  84. email_verified = models.BooleanField(default=False)
  85. checks = models.ManyToManyField(Check)
  86. def make_token(self):
  87. seed = "%s%s" % (self.code, settings.SECRET_KEY)
  88. seed = seed.encode("utf8")
  89. return hashlib.sha1(seed).hexdigest()
  90. def send_verify_link(self):
  91. args = [self.code, self.make_token()]
  92. verify_link = reverse("hc-verify-email", args=args)
  93. verify_link = settings.SITE_ROOT + verify_link
  94. emails.verify_email(self.value, {"verify_link": verify_link})
  95. def notify(self, check):
  96. n = Notification(owner=check, channel=self)
  97. n.check_status = check.status
  98. if self.kind == "email" and self.email_verified:
  99. ctx = {
  100. "check": check,
  101. "checks": self.user.check_set.order_by("created"),
  102. "now": timezone.now()
  103. }
  104. emails.alert(self.value, ctx)
  105. n.save()
  106. elif self.kind == "webhook" and check.status == "down":
  107. try:
  108. r = requests.get(self.value, timeout=5)
  109. n.status = r.status_code
  110. except requests.exceptions.Timeout:
  111. # Well, we tried
  112. pass
  113. n.save()
  114. elif self.kind == "slack":
  115. tmpl = "integrations/slack_message.html"
  116. text = render_to_string(tmpl, {"check": check})
  117. payload = {
  118. "text": text,
  119. "username": "healthchecks.io",
  120. "icon_url": "https://healthchecks.io/static/img/[email protected]"
  121. }
  122. r = requests.post(self.value, json=payload, timeout=5)
  123. n.status = r.status_code
  124. n.save()
  125. elif self.kind == "hipchat":
  126. tmpl = "integrations/hipchat_message.html"
  127. text = render_to_string(tmpl, {"check": check})
  128. payload = {
  129. "message": text,
  130. "color": "green" if check.status == "up" else "red",
  131. }
  132. r = requests.post(self.value, json=payload, timeout=5)
  133. n.status = r.status_code
  134. n.save()
  135. elif self.kind == "pd":
  136. if check.status == "down":
  137. event_type = "trigger"
  138. description = "%s is DOWN" % check.name_then_code()
  139. else:
  140. event_type = "resolve"
  141. description = "%s received a ping and is now UP" % \
  142. check.name_then_code()
  143. payload = {
  144. "service_key": self.value,
  145. "incident_key": str(check.code),
  146. "event_type": event_type,
  147. "description": description,
  148. "client": "healthchecks.io",
  149. "client_url": settings.SITE_ROOT
  150. }
  151. url = "https://events.pagerduty.com/generic/2010-04-15/create_event.json"
  152. r = requests.post(url, data=json.dumps(payload), timeout=5)
  153. n.status = r.status_code
  154. n.save()
  155. elif self.kind == "po":
  156. tmpl = "integrations/pushover_message.html"
  157. ctx = {
  158. "check": check,
  159. "down_checks": self.user.check_set.filter(status="down").exclude(code=check.code).order_by("created"),
  160. }
  161. text = render_to_string(tmpl, ctx).strip()
  162. if check.status == "down":
  163. title = "%s is DOWN" % check.name_then_code()
  164. else:
  165. title = "%s is now UP" % check.name_then_code()
  166. user_key, priority, _ = self.po_value
  167. payload = {
  168. "token": settings.PUSHOVER_API_TOKEN,
  169. "user": user_key,
  170. "message": text,
  171. "title": title,
  172. "html": 1,
  173. "priority": priority,
  174. }
  175. if priority == 2: # Emergency notification
  176. payload["retry"] = settings.PUSHOVER_EMERGENCY_RETRY_DELAY
  177. payload["expire"] = settings.PUSHOVER_EMERGENCY_EXPIRATION
  178. url = "https://api.pushover.net/1/messages.json"
  179. r = requests.post(url, data=payload, timeout=5)
  180. n.status = r.status_code
  181. n.save()
  182. @property
  183. def po_value(self):
  184. assert self.kind == "po"
  185. user_key, prio = self.value.split("|")
  186. prio = int(prio)
  187. return user_key, prio, PO_PRIORITIES[prio]
  188. class Notification(models.Model):
  189. owner = models.ForeignKey(Check)
  190. check_status = models.CharField(max_length=6)
  191. channel = models.ForeignKey(Channel)
  192. created = models.DateTimeField(auto_now_add=True)
  193. status = models.IntegerField(default=0)