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.

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