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.

1613 lines
49 KiB

6 years ago
9 years ago
9 years ago
8 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
8 years ago
9 years ago
10 years ago
10 years ago
10 years ago
6 years ago
10 years ago
10 years ago
6 years ago
9 years ago
6 years ago
6 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
8 years ago
8 years ago
  1. from datetime import datetime, timedelta as td
  2. import json
  3. import os
  4. from urllib.parse import urlencode
  5. from croniter import croniter
  6. from django.conf import settings
  7. from django.contrib import messages
  8. from django.contrib.auth.decorators import login_required
  9. from django.core import signing
  10. from django.db.models import Count
  11. from django.http import (
  12. Http404,
  13. HttpResponse,
  14. HttpResponseBadRequest,
  15. HttpResponseForbidden,
  16. JsonResponse,
  17. )
  18. from django.shortcuts import get_object_or_404, redirect, render
  19. from django.template.loader import get_template, render_to_string
  20. from django.urls import reverse
  21. from django.utils import timezone
  22. from django.utils.crypto import get_random_string
  23. from django.views.decorators.csrf import csrf_exempt
  24. from django.views.decorators.http import require_POST
  25. from hc.accounts.models import Project
  26. from hc.api.models import (
  27. DEFAULT_GRACE,
  28. DEFAULT_TIMEOUT,
  29. MAX_DELTA,
  30. Channel,
  31. Check,
  32. Ping,
  33. Notification,
  34. )
  35. from hc.api.transports import Telegram
  36. from hc.front.forms import (
  37. AddAppriseForm,
  38. AddEmailForm,
  39. AddMatrixForm,
  40. AddOpsGenieForm,
  41. AddPdForm,
  42. AddShellForm,
  43. AddSmsForm,
  44. AddUrlForm,
  45. AddWebhookForm,
  46. ChannelNameForm,
  47. CronForm,
  48. FilteringRulesForm,
  49. NameTagsForm,
  50. TimeoutForm,
  51. )
  52. from hc.front.schemas import telegram_callback
  53. from hc.front.templatetags.hc_extras import num_down_title, down_title, sortchecks
  54. from hc.lib import jsonschema
  55. from hc.lib.badges import get_badge_url
  56. import pytz
  57. from pytz.exceptions import UnknownTimeZoneError
  58. import requests
  59. VALID_SORT_VALUES = ("name", "-name", "last_ping", "-last_ping", "created")
  60. STATUS_TEXT_TMPL = get_template("front/log_status_text.html")
  61. LAST_PING_TMPL = get_template("front/last_ping_cell.html")
  62. EVENTS_TMPL = get_template("front/details_events.html")
  63. DOWNTIMES_TMPL = get_template("front/details_downtimes.html")
  64. def _tags_statuses(checks):
  65. tags, down, grace, num_down = {}, {}, {}, 0
  66. for check in checks:
  67. status = check.get_status(with_started=False)
  68. if status == "down":
  69. num_down += 1
  70. for tag in check.tags_list():
  71. down[tag] = "down"
  72. elif status == "grace":
  73. for tag in check.tags_list():
  74. grace[tag] = "grace"
  75. else:
  76. for tag in check.tags_list():
  77. tags[tag] = "up"
  78. tags.update(grace)
  79. tags.update(down)
  80. return tags, num_down
  81. def _get_check_for_user(request, code):
  82. """ Return specified check if current user has access to it. """
  83. if not request.user.is_authenticated:
  84. raise Http404("not found")
  85. if request.user.is_superuser:
  86. q = Check.objects
  87. else:
  88. q = request.profile.checks_from_all_projects()
  89. try:
  90. return q.get(code=code)
  91. except Check.DoesNotExist:
  92. raise Http404("not found")
  93. def _get_project_for_user(request, project_code):
  94. """ Return true if current user has access to the specified account. """
  95. if request.user.is_superuser:
  96. q = Project.objects
  97. else:
  98. q = request.profile.projects()
  99. try:
  100. return q.get(code=project_code)
  101. except Project.DoesNotExist:
  102. raise Http404("not found")
  103. def _refresh_last_active_date(profile):
  104. """ Update last_active_date if it is more than a day old. """
  105. now = timezone.now()
  106. if profile.last_active_date is None or (now - profile.last_active_date).days > 0:
  107. profile.last_active_date = now
  108. profile.save()
  109. @login_required
  110. def my_checks(request, code):
  111. _refresh_last_active_date(request.profile)
  112. project = _get_project_for_user(request, code)
  113. if request.GET.get("sort") in VALID_SORT_VALUES:
  114. request.profile.sort = request.GET["sort"]
  115. request.profile.save()
  116. if request.profile.current_project_id != project.id:
  117. request.profile.current_project = project
  118. request.profile.save()
  119. q = Check.objects.filter(project=project)
  120. checks = list(q.prefetch_related("channel_set"))
  121. sortchecks(checks, request.profile.sort)
  122. tags_statuses, num_down = _tags_statuses(checks)
  123. pairs = list(tags_statuses.items())
  124. pairs.sort(key=lambda pair: pair[0].lower())
  125. channels = Channel.objects.filter(project=project)
  126. channels = list(channels.order_by("created"))
  127. hidden_checks = set()
  128. # Hide checks that don't match selected tags:
  129. selected_tags = set(request.GET.getlist("tag", []))
  130. if selected_tags:
  131. for check in checks:
  132. if not selected_tags.issubset(check.tags_list()):
  133. hidden_checks.add(check)
  134. # Hide checks that don't match the search string:
  135. search = request.GET.get("search", "")
  136. if search:
  137. for check in checks:
  138. search_key = "%s\n%s" % (check.name.lower(), check.code)
  139. if search not in search_key:
  140. hidden_checks.add(check)
  141. # Do we need to show the "Last Duration" header?
  142. show_last_duration = False
  143. for check in checks:
  144. if check.clamped_last_duration():
  145. show_last_duration = True
  146. break
  147. ctx = {
  148. "page": "checks",
  149. "checks": checks,
  150. "channels": channels,
  151. "num_down": num_down,
  152. "now": timezone.now(),
  153. "tags": pairs,
  154. "ping_endpoint": settings.PING_ENDPOINT,
  155. "timezones": pytz.all_timezones,
  156. "project": project,
  157. "num_available": project.num_checks_available(),
  158. "sort": request.profile.sort,
  159. "selected_tags": selected_tags,
  160. "search": search,
  161. "hidden_checks": hidden_checks,
  162. "show_last_duration": show_last_duration,
  163. }
  164. return render(request, "front/my_checks.html", ctx)
  165. @login_required
  166. def status(request, code):
  167. _get_project_for_user(request, code)
  168. checks = list(Check.objects.filter(project__code=code))
  169. details = []
  170. for check in checks:
  171. ctx = {"check": check}
  172. details.append(
  173. {
  174. "code": str(check.code),
  175. "status": check.get_status(),
  176. "last_ping": LAST_PING_TMPL.render(ctx),
  177. }
  178. )
  179. tags_statuses, num_down = _tags_statuses(checks)
  180. return JsonResponse(
  181. {"details": details, "tags": tags_statuses, "title": num_down_title(num_down)}
  182. )
  183. @login_required
  184. @require_POST
  185. def switch_channel(request, code, channel_code):
  186. check = _get_check_for_user(request, code)
  187. channel = get_object_or_404(Channel, code=channel_code)
  188. if channel.project_id != check.project_id:
  189. return HttpResponseBadRequest()
  190. if request.POST.get("state") == "on":
  191. channel.checks.add(check)
  192. else:
  193. channel.checks.remove(check)
  194. return HttpResponse()
  195. def index(request):
  196. if request.user.is_authenticated:
  197. projects = list(request.profile.projects())
  198. ctx = {"page": "projects", "projects": projects}
  199. return render(request, "front/projects.html", ctx)
  200. check = Check()
  201. ctx = {
  202. "page": "welcome",
  203. "check": check,
  204. "ping_url": check.url(),
  205. "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None,
  206. "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
  207. "enable_discord": settings.DISCORD_CLIENT_ID is not None,
  208. "enable_telegram": settings.TELEGRAM_TOKEN is not None,
  209. "enable_sms": settings.TWILIO_AUTH is not None,
  210. "enable_whatsapp": settings.TWILIO_USE_WHATSAPP,
  211. "enable_trello": settings.TRELLO_APP_KEY is not None,
  212. "enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
  213. "enable_apprise": settings.APPRISE_ENABLED is True,
  214. "enable_shell": settings.SHELL_ENABLED is True,
  215. "registration_open": settings.REGISTRATION_OPEN,
  216. }
  217. return render(request, "front/welcome.html", ctx)
  218. def serve_doc(request, doc="introduction"):
  219. path = os.path.join(settings.BASE_DIR, "templates/docs", doc + ".html")
  220. if not os.path.exists(path):
  221. raise Http404("not found")
  222. content = open(path, "r", encoding="utf-8").read()
  223. content = content.replace("SITE_NAME", settings.SITE_NAME)
  224. content = content.replace("PING_URL", settings.PING_ENDPOINT + "your-uuid-here")
  225. content = content.replace("PING_ENDPOINT", settings.PING_ENDPOINT)
  226. content = content.replace("IMG_URL", os.path.join(settings.STATIC_URL, "img/docs"))
  227. content = content.replace(
  228. "PING_EMAIL", "your-uuid-here@%s" % settings.PING_EMAIL_DOMAIN
  229. )
  230. ctx = {
  231. "page": "docs",
  232. "section": "home",
  233. "section": doc,
  234. "content": content,
  235. "first_line": content.split("\n")[0],
  236. }
  237. return render(request, "front/docs_single.html", ctx)
  238. def docs_api(request):
  239. ctx = {
  240. "page": "docs",
  241. "section": "api",
  242. "SITE_ROOT": settings.SITE_ROOT,
  243. "PING_ENDPOINT": settings.PING_ENDPOINT,
  244. "default_timeout": int(DEFAULT_TIMEOUT.total_seconds()),
  245. "default_grace": int(DEFAULT_GRACE.total_seconds()),
  246. }
  247. return render(request, "front/docs_api.html", ctx)
  248. def docs_cron(request):
  249. return render(request, "front/docs_cron.html", {})
  250. @require_POST
  251. @login_required
  252. def add_check(request, code):
  253. project = _get_project_for_user(request, code)
  254. if project.num_checks_available() <= 0:
  255. return HttpResponseBadRequest()
  256. check = Check(project=project)
  257. check.save()
  258. check.assign_all_channels()
  259. url = reverse("hc-details", args=[check.code])
  260. return redirect(url + "?new")
  261. @require_POST
  262. @login_required
  263. def update_name(request, code):
  264. check = _get_check_for_user(request, code)
  265. form = NameTagsForm(request.POST)
  266. if form.is_valid():
  267. check.name = form.cleaned_data["name"]
  268. check.tags = form.cleaned_data["tags"]
  269. check.desc = form.cleaned_data["desc"]
  270. check.save()
  271. if "/details/" in request.META.get("HTTP_REFERER", ""):
  272. return redirect("hc-details", code)
  273. return redirect("hc-checks", check.project.code)
  274. @require_POST
  275. @login_required
  276. def filtering_rules(request, code):
  277. check = _get_check_for_user(request, code)
  278. form = FilteringRulesForm(request.POST)
  279. if form.is_valid():
  280. check.subject = form.cleaned_data["subject"]
  281. check.methods = form.cleaned_data["methods"]
  282. check.save()
  283. return redirect("hc-details", code)
  284. @require_POST
  285. @login_required
  286. def update_timeout(request, code):
  287. check = _get_check_for_user(request, code)
  288. kind = request.POST.get("kind")
  289. if kind == "simple":
  290. form = TimeoutForm(request.POST)
  291. if not form.is_valid():
  292. return HttpResponseBadRequest()
  293. check.kind = "simple"
  294. check.timeout = form.cleaned_data["timeout"]
  295. check.grace = form.cleaned_data["grace"]
  296. elif kind == "cron":
  297. form = CronForm(request.POST)
  298. if not form.is_valid():
  299. return HttpResponseBadRequest()
  300. check.kind = "cron"
  301. check.schedule = form.cleaned_data["schedule"]
  302. check.tz = form.cleaned_data["tz"]
  303. check.grace = td(minutes=form.cleaned_data["grace"])
  304. check.alert_after = check.going_down_after()
  305. if check.status == "up" and check.alert_after < timezone.now():
  306. # Checks can flip from "up" to "down" state as a result of changing check's
  307. # schedule. We don't want to send notifications when changing schedule
  308. # interactively in the web UI. So we update the `alert_after` and `status`
  309. # fields here the same way as `sendalerts` would do, but without sending
  310. # an actual alert:
  311. check.alert_after = None
  312. check.status = "down"
  313. check.save()
  314. if "/details/" in request.META.get("HTTP_REFERER", ""):
  315. return redirect("hc-details", code)
  316. return redirect("hc-checks", check.project.code)
  317. @require_POST
  318. def cron_preview(request):
  319. schedule = request.POST.get("schedule", "")
  320. tz = request.POST.get("tz")
  321. ctx = {"tz": tz, "dates": []}
  322. try:
  323. zone = pytz.timezone(tz)
  324. now_local = timezone.localtime(timezone.now(), zone)
  325. if len(schedule.split()) != 5:
  326. raise ValueError()
  327. it = croniter(schedule, now_local)
  328. for i in range(0, 6):
  329. ctx["dates"].append(it.get_next(datetime))
  330. except UnknownTimeZoneError:
  331. ctx["bad_tz"] = True
  332. except:
  333. ctx["bad_schedule"] = True
  334. return render(request, "front/cron_preview.html", ctx)
  335. def ping_details(request, code, n=None):
  336. check = _get_check_for_user(request, code)
  337. q = Ping.objects.filter(owner=check)
  338. if n:
  339. q = q.filter(n=n)
  340. try:
  341. ping = q.latest("created")
  342. except Ping.DoesNotExist:
  343. return render(request, "front/ping_details_not_found.html")
  344. ctx = {"check": check, "ping": ping}
  345. return render(request, "front/ping_details.html", ctx)
  346. @require_POST
  347. @login_required
  348. def pause(request, code):
  349. check = _get_check_for_user(request, code)
  350. check.status = "paused"
  351. check.last_start = None
  352. check.alert_after = None
  353. check.save()
  354. if "/details/" in request.META.get("HTTP_REFERER", ""):
  355. return redirect("hc-details", code)
  356. # Don't redirect after an AJAX request:
  357. if request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest":
  358. return HttpResponse()
  359. return redirect("hc-checks", check.project.code)
  360. @require_POST
  361. @login_required
  362. def remove_check(request, code):
  363. check = _get_check_for_user(request, code)
  364. project = check.project
  365. check.delete()
  366. return redirect("hc-checks", project.code)
  367. def _get_events(check, limit):
  368. pings = Ping.objects.filter(owner=check).order_by("-id")[:limit]
  369. pings = list(pings)
  370. prev = None
  371. for ping in pings:
  372. if ping.kind == "start" and prev and prev.kind != "start":
  373. delta = prev.created - ping.created
  374. if delta < MAX_DELTA:
  375. setattr(prev, "delta", delta)
  376. prev = ping
  377. alerts = []
  378. if len(pings):
  379. cutoff = pings[-1].created
  380. alerts = Notification.objects.select_related("channel").filter(
  381. owner=check, check_status="down", created__gt=cutoff
  382. )
  383. events = pings + list(alerts)
  384. events.sort(key=lambda el: el.created, reverse=True)
  385. return events
  386. @login_required
  387. def log(request, code):
  388. check = _get_check_for_user(request, code)
  389. limit = check.project.owner_profile.ping_log_limit
  390. ctx = {
  391. "project": check.project,
  392. "check": check,
  393. "events": _get_events(check, limit),
  394. "limit": limit,
  395. "show_limit_notice": check.n_pings > limit and settings.USE_PAYMENTS,
  396. }
  397. return render(request, "front/log.html", ctx)
  398. @login_required
  399. def details(request, code):
  400. _refresh_last_active_date(request.profile)
  401. check = _get_check_for_user(request, code)
  402. channels = Channel.objects.filter(project=check.project)
  403. channels = list(channels.order_by("created"))
  404. ctx = {
  405. "page": "details",
  406. "project": check.project,
  407. "check": check,
  408. "channels": channels,
  409. "timezones": pytz.all_timezones,
  410. "downtimes": check.downtimes(months=3),
  411. "is_new": "new" in request.GET,
  412. "is_copied": "copied" in request.GET,
  413. }
  414. return render(request, "front/details.html", ctx)
  415. @login_required
  416. def transfer(request, code):
  417. check = _get_check_for_user(request, code)
  418. if request.method == "POST":
  419. target_project = _get_project_for_user(request, request.POST["project"])
  420. if target_project.num_checks_available() <= 0:
  421. return HttpResponseBadRequest()
  422. check.project = target_project
  423. check.save()
  424. check.assign_all_channels()
  425. request.profile.current_project = target_project
  426. request.profile.save()
  427. messages.success(request, "Check transferred successfully!")
  428. return redirect("hc-details", code)
  429. ctx = {"check": check}
  430. return render(request, "front/transfer_modal.html", ctx)
  431. @require_POST
  432. @login_required
  433. def copy(request, code):
  434. check = _get_check_for_user(request, code)
  435. copied = Check(project=check.project)
  436. copied.name = check.name + " (copy)"
  437. copied.desc, copied.tags = check.desc, check.tags
  438. copied.subject = check.subject
  439. copied.kind = check.kind
  440. copied.timeout, copied.grace = check.timeout, check.grace
  441. copied.schedule, copied.tz = check.schedule, check.tz
  442. copied.save()
  443. copied.channel_set.add(*check.channel_set.all())
  444. url = reverse("hc-details", args=[copied.code])
  445. return redirect(url + "?copied")
  446. @login_required
  447. def status_single(request, code):
  448. check = _get_check_for_user(request, code)
  449. status = check.get_status()
  450. events = _get_events(check, 20)
  451. updated = "1"
  452. if len(events):
  453. updated = str(events[0].created.timestamp())
  454. doc = {
  455. "status": status,
  456. "status_text": STATUS_TEXT_TMPL.render({"check": check}),
  457. "title": down_title(check),
  458. "updated": updated,
  459. }
  460. if updated != request.GET.get("u"):
  461. doc["events"] = EVENTS_TMPL.render({"check": check, "events": events})
  462. doc["downtimes"] = DOWNTIMES_TMPL.render({"downtimes": check.downtimes(3)})
  463. return JsonResponse(doc)
  464. @login_required
  465. def badges(request, code):
  466. project = _get_project_for_user(request, code)
  467. tags = set()
  468. for check in Check.objects.filter(project=project):
  469. tags.update(check.tags_list())
  470. sorted_tags = sorted(tags, key=lambda s: s.lower())
  471. sorted_tags.append("*") # For the "overall status" badge
  472. urls = []
  473. for tag in sorted_tags:
  474. urls.append(
  475. {
  476. "tag": tag,
  477. "svg": get_badge_url(project.badge_key, tag),
  478. "json": get_badge_url(project.badge_key, tag, fmt="json"),
  479. "shields": get_badge_url(project.badge_key, tag, fmt="shields"),
  480. }
  481. )
  482. ctx = {
  483. "have_tags": len(urls) > 1,
  484. "page": "badges",
  485. "project": project,
  486. "badges": urls,
  487. }
  488. return render(request, "front/badges.html", ctx)
  489. @login_required
  490. def channels(request, code=None):
  491. if code:
  492. project = _get_project_for_user(request, code)
  493. else:
  494. project = request.project
  495. if project is None:
  496. # This can happen when the user deletes their only project.
  497. return redirect("hc-index")
  498. if request.method == "POST":
  499. code = request.POST["channel"]
  500. try:
  501. channel = Channel.objects.get(code=code)
  502. except Channel.DoesNotExist:
  503. return HttpResponseBadRequest()
  504. if channel.project_id != project.id:
  505. return HttpResponseForbidden()
  506. new_checks = []
  507. for key in request.POST:
  508. if key.startswith("check-"):
  509. code = key[6:]
  510. try:
  511. check = Check.objects.get(code=code)
  512. except Check.DoesNotExist:
  513. return HttpResponseBadRequest()
  514. if check.project_id != project.id:
  515. return HttpResponseForbidden()
  516. new_checks.append(check)
  517. channel.checks.set(new_checks)
  518. return redirect("hc-channels")
  519. channels = Channel.objects.filter(project=project)
  520. channels = channels.order_by("created")
  521. channels = channels.annotate(n_checks=Count("checks"))
  522. ctx = {
  523. "page": "channels",
  524. "project": project,
  525. "profile": project.owner_profile,
  526. "channels": channels,
  527. "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None,
  528. "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
  529. "enable_discord": settings.DISCORD_CLIENT_ID is not None,
  530. "enable_telegram": settings.TELEGRAM_TOKEN is not None,
  531. "enable_sms": settings.TWILIO_AUTH is not None,
  532. "enable_whatsapp": settings.TWILIO_USE_WHATSAPP,
  533. "enable_pdc": settings.PD_VENDOR_KEY is not None,
  534. "enable_trello": settings.TRELLO_APP_KEY is not None,
  535. "enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
  536. "enable_apprise": settings.APPRISE_ENABLED is True,
  537. "enable_shell": settings.SHELL_ENABLED is True,
  538. "use_payments": settings.USE_PAYMENTS,
  539. }
  540. return render(request, "front/channels.html", ctx)
  541. @login_required
  542. def channel_checks(request, code):
  543. channel = get_object_or_404(Channel, code=code)
  544. if channel.project_id != request.project.id:
  545. return HttpResponseForbidden()
  546. assigned = set(channel.checks.values_list("code", flat=True).distinct())
  547. checks = Check.objects.filter(project=request.project).order_by("created")
  548. ctx = {"checks": checks, "assigned": assigned, "channel": channel}
  549. return render(request, "front/channel_checks.html", ctx)
  550. @require_POST
  551. @login_required
  552. def update_channel_name(request, code):
  553. channel = get_object_or_404(Channel, code=code)
  554. if channel.project_id != request.project.id:
  555. return HttpResponseForbidden()
  556. form = ChannelNameForm(request.POST)
  557. if form.is_valid():
  558. channel.name = form.cleaned_data["name"]
  559. channel.save()
  560. return redirect("hc-channels")
  561. def verify_email(request, code, token):
  562. channel = get_object_or_404(Channel, code=code)
  563. if channel.make_token() == token:
  564. channel.email_verified = True
  565. channel.save()
  566. return render(request, "front/verify_email_success.html")
  567. return render(request, "bad_link.html")
  568. @csrf_exempt
  569. def unsubscribe_email(request, code, signed_token):
  570. # Some email servers open links in emails to check for malicious content.
  571. # To work around this, on GET requests we serve a confirmation form.
  572. # If the signature is at least 5 minutes old, we also include JS code to
  573. # auto-submit the form.
  574. ctx = {}
  575. if ":" in signed_token:
  576. signer = signing.TimestampSigner(salt="alerts")
  577. # First, check the signature without looking at the timestamp:
  578. try:
  579. token = signer.unsign(signed_token)
  580. except signing.BadSignature:
  581. return render(request, "bad_link.html")
  582. # Check if timestamp is older than 5 minutes:
  583. try:
  584. signer.unsign(signed_token, max_age=300)
  585. except signing.SignatureExpired:
  586. ctx["autosubmit"] = True
  587. else:
  588. token = signed_token
  589. channel = get_object_or_404(Channel, code=code, kind="email")
  590. if channel.make_token() != token:
  591. return render(request, "bad_link.html")
  592. if request.method != "POST":
  593. return render(request, "accounts/unsubscribe_submit.html", ctx)
  594. channel.delete()
  595. return render(request, "front/unsubscribe_success.html")
  596. @require_POST
  597. @login_required
  598. def send_test_notification(request, code):
  599. channel = get_object_or_404(Channel, code=code)
  600. if channel.project_id != request.project.id:
  601. return HttpResponseForbidden()
  602. dummy = Check(name="TEST", status="down")
  603. dummy.last_ping = timezone.now() - td(days=1)
  604. dummy.n_pings = 42
  605. if channel.kind == "webhook" and not channel.url_down:
  606. if channel.url_up:
  607. # If we don't have url_down, but do have have url_up then
  608. # send "TEST is UP" notification instead:
  609. dummy.status = "up"
  610. if channel.kind == "email":
  611. error = channel.transport.notify(dummy, channel.get_unsub_link())
  612. else:
  613. error = channel.transport.notify(dummy)
  614. if error:
  615. messages.warning(request, "Could not send a test notification: %s" % error)
  616. else:
  617. messages.success(request, "Test notification sent!")
  618. return redirect("hc-channels")
  619. @require_POST
  620. @login_required
  621. def remove_channel(request, code):
  622. # user may refresh the page during POST and cause two deletion attempts
  623. channel = Channel.objects.filter(code=code).first()
  624. if channel:
  625. if channel.project_id != request.project.id:
  626. return HttpResponseForbidden()
  627. channel.delete()
  628. return redirect("hc-channels")
  629. @login_required
  630. def add_email(request, code):
  631. project = _get_project_for_user(request, code)
  632. if request.method == "POST":
  633. form = AddEmailForm(request.POST)
  634. if form.is_valid():
  635. channel = Channel(project=project, kind="email")
  636. channel.value = json.dumps(
  637. {
  638. "value": form.cleaned_data["value"],
  639. "up": form.cleaned_data["up"],
  640. "down": form.cleaned_data["down"],
  641. }
  642. )
  643. channel.save()
  644. channel.assign_all_checks()
  645. is_own_email = form.cleaned_data["value"] == request.user.email
  646. if is_own_email or not settings.EMAIL_USE_VERIFICATION:
  647. # If user is subscribing *their own* address
  648. # we can skip the verification step.
  649. # Additionally, in self-hosted setting, administator has the
  650. # option to disable the email verification step altogether.
  651. channel.email_verified = True
  652. channel.save()
  653. else:
  654. channel.send_verify_link()
  655. return redirect("hc-p-channels", project.code)
  656. else:
  657. form = AddEmailForm()
  658. ctx = {
  659. "page": "channels",
  660. "project": project,
  661. "use_verification": settings.EMAIL_USE_VERIFICATION,
  662. "form": form,
  663. }
  664. return render(request, "integrations/add_email.html", ctx)
  665. @login_required
  666. def add_webhook(request, code):
  667. project = _get_project_for_user(request, code)
  668. if request.method == "POST":
  669. form = AddWebhookForm(request.POST)
  670. if form.is_valid():
  671. channel = Channel(project=project, kind="webhook")
  672. channel.value = form.get_value()
  673. channel.save()
  674. channel.assign_all_checks()
  675. return redirect("hc-p-channels", project.code)
  676. else:
  677. form = AddWebhookForm()
  678. ctx = {
  679. "page": "channels",
  680. "project": project,
  681. "form": form,
  682. "now": timezone.now().replace(microsecond=0).isoformat(),
  683. }
  684. return render(request, "integrations/add_webhook.html", ctx)
  685. @login_required
  686. def add_shell(request):
  687. if not settings.SHELL_ENABLED:
  688. raise Http404("shell integration is not available")
  689. if request.method == "POST":
  690. form = AddShellForm(request.POST)
  691. if form.is_valid():
  692. channel = Channel(project=request.project, kind="shell")
  693. channel.value = form.get_value()
  694. channel.save()
  695. channel.assign_all_checks()
  696. return redirect("hc-channels")
  697. else:
  698. form = AddShellForm()
  699. ctx = {
  700. "page": "channels",
  701. "project": request.project,
  702. "form": form,
  703. "now": timezone.now().replace(microsecond=0).isoformat(),
  704. }
  705. return render(request, "integrations/add_shell.html", ctx)
  706. def _prepare_state(request, session_key):
  707. state = get_random_string()
  708. request.session[session_key] = state
  709. return state
  710. def _get_validated_code(request, session_key, key="code"):
  711. if session_key not in request.session:
  712. return None
  713. session_state = request.session.pop(session_key)
  714. request_state = request.GET.get("state")
  715. if session_state is None or session_state != request_state:
  716. return None
  717. return request.GET.get(key)
  718. @login_required
  719. def add_pd(request):
  720. if request.method == "POST":
  721. form = AddPdForm(request.POST)
  722. if form.is_valid():
  723. channel = Channel(project=request.project, kind="pd")
  724. channel.value = form.cleaned_data["value"]
  725. channel.save()
  726. channel.assign_all_checks()
  727. return redirect("hc-channels")
  728. else:
  729. form = AddPdForm()
  730. ctx = {"page": "channels", "form": form}
  731. return render(request, "integrations/add_pd.html", ctx)
  732. def add_pdc(request, state=None):
  733. if settings.PD_VENDOR_KEY is None:
  734. raise Http404("pagerduty integration is not available")
  735. if state and request.user.is_authenticated:
  736. if "pd" not in request.session:
  737. return HttpResponseBadRequest()
  738. session_state = request.session.pop("pd")
  739. if session_state != state:
  740. return HttpResponseBadRequest()
  741. if request.GET.get("error") == "cancelled":
  742. messages.warning(request, "PagerDuty setup was cancelled")
  743. return redirect("hc-channels")
  744. channel = Channel(kind="pd", project=request.project)
  745. channel.user = request.project.owner
  746. channel.value = json.dumps(
  747. {
  748. "service_key": request.GET.get("service_key"),
  749. "account": request.GET.get("account"),
  750. }
  751. )
  752. channel.save()
  753. channel.assign_all_checks()
  754. messages.success(request, "The PagerDuty integration has been added!")
  755. return redirect("hc-channels")
  756. state = _prepare_state(request, "pd")
  757. callback = settings.SITE_ROOT + reverse("hc-add-pdc-state", args=[state])
  758. connect_url = "https://connect.pagerduty.com/connect?" + urlencode(
  759. {"vendor": settings.PD_VENDOR_KEY, "callback": callback}
  760. )
  761. ctx = {"page": "channels", "project": request.project, "connect_url": connect_url}
  762. return render(request, "integrations/add_pdc.html", ctx)
  763. @login_required
  764. def add_pagertree(request, code):
  765. project = _get_project_for_user(request, code)
  766. if request.method == "POST":
  767. form = AddUrlForm(request.POST)
  768. if form.is_valid():
  769. channel = Channel(project=project, kind="pagertree")
  770. channel.value = form.cleaned_data["value"]
  771. channel.save()
  772. channel.assign_all_checks()
  773. return redirect("hc-p-channels", project.code)
  774. else:
  775. form = AddUrlForm()
  776. ctx = {"page": "channels", "project": project, "form": form}
  777. return render(request, "integrations/add_pagertree.html", ctx)
  778. @login_required
  779. def add_pagerteam(request, code):
  780. project = _get_project_for_user(request, code)
  781. if request.method == "POST":
  782. form = AddUrlForm(request.POST)
  783. if form.is_valid():
  784. channel = Channel(project=project, kind="pagerteam")
  785. channel.value = form.cleaned_data["value"]
  786. channel.save()
  787. channel.assign_all_checks()
  788. return redirect("hc-p-channels", project.code)
  789. else:
  790. form = AddUrlForm()
  791. ctx = {"page": "channels", "project": project, "form": form}
  792. return render(request, "integrations/add_pagerteam.html", ctx)
  793. def add_slack(request):
  794. if not settings.SLACK_CLIENT_ID and not request.user.is_authenticated:
  795. return redirect("hc-login")
  796. if request.method == "POST":
  797. form = AddUrlForm(request.POST)
  798. if form.is_valid():
  799. channel = Channel(project=request.project, kind="slack")
  800. channel.value = form.cleaned_data["value"]
  801. channel.save()
  802. channel.assign_all_checks()
  803. return redirect("hc-channels")
  804. else:
  805. form = AddUrlForm()
  806. ctx = {
  807. "page": "channels",
  808. "form": form,
  809. "slack_client_id": settings.SLACK_CLIENT_ID,
  810. }
  811. if request.user.is_authenticated:
  812. ctx["project"] = request.project
  813. if settings.SLACK_CLIENT_ID and request.user.is_authenticated:
  814. ctx["state"] = _prepare_state(request, "slack")
  815. return render(request, "integrations/add_slack.html", ctx)
  816. @login_required
  817. def add_mattermost(request, code):
  818. project = _get_project_for_user(request, code)
  819. if request.method == "POST":
  820. form = AddUrlForm(request.POST)
  821. if form.is_valid():
  822. channel = Channel(project=project, kind="mattermost")
  823. channel.value = form.cleaned_data["value"]
  824. channel.save()
  825. channel.assign_all_checks()
  826. return redirect("hc-p-channels", project.code)
  827. else:
  828. form = AddUrlForm()
  829. ctx = {"page": "channels", "form": form, "project": project}
  830. return render(request, "integrations/add_mattermost.html", ctx)
  831. @login_required
  832. def add_slack_btn(request):
  833. code = _get_validated_code(request, "slack")
  834. if code is None:
  835. return HttpResponseBadRequest()
  836. result = requests.post(
  837. "https://slack.com/api/oauth.access",
  838. {
  839. "client_id": settings.SLACK_CLIENT_ID,
  840. "client_secret": settings.SLACK_CLIENT_SECRET,
  841. "code": code,
  842. },
  843. )
  844. doc = result.json()
  845. if doc.get("ok"):
  846. channel = Channel(kind="slack", project=request.project)
  847. channel.user = request.project.owner
  848. channel.value = result.text
  849. channel.save()
  850. channel.assign_all_checks()
  851. messages.success(request, "The Slack integration has been added!")
  852. else:
  853. s = doc.get("error")
  854. messages.warning(request, "Error message from slack: %s" % s)
  855. return redirect("hc-channels")
  856. @login_required
  857. def add_pushbullet(request):
  858. if settings.PUSHBULLET_CLIENT_ID is None:
  859. raise Http404("pushbullet integration is not available")
  860. if "code" in request.GET:
  861. code = _get_validated_code(request, "pushbullet")
  862. if code is None:
  863. return HttpResponseBadRequest()
  864. result = requests.post(
  865. "https://api.pushbullet.com/oauth2/token",
  866. {
  867. "client_id": settings.PUSHBULLET_CLIENT_ID,
  868. "client_secret": settings.PUSHBULLET_CLIENT_SECRET,
  869. "code": code,
  870. "grant_type": "authorization_code",
  871. },
  872. )
  873. doc = result.json()
  874. if "access_token" in doc:
  875. channel = Channel(kind="pushbullet", project=request.project)
  876. channel.user = request.project.owner
  877. channel.value = doc["access_token"]
  878. channel.save()
  879. channel.assign_all_checks()
  880. messages.success(request, "The Pushbullet integration has been added!")
  881. else:
  882. messages.warning(request, "Something went wrong")
  883. return redirect("hc-channels")
  884. redirect_uri = settings.SITE_ROOT + reverse("hc-add-pushbullet")
  885. authorize_url = "https://www.pushbullet.com/authorize?" + urlencode(
  886. {
  887. "client_id": settings.PUSHBULLET_CLIENT_ID,
  888. "redirect_uri": redirect_uri,
  889. "response_type": "code",
  890. "state": _prepare_state(request, "pushbullet"),
  891. }
  892. )
  893. ctx = {
  894. "page": "channels",
  895. "project": request.project,
  896. "authorize_url": authorize_url,
  897. }
  898. return render(request, "integrations/add_pushbullet.html", ctx)
  899. @login_required
  900. def add_discord(request):
  901. if settings.DISCORD_CLIENT_ID is None:
  902. raise Http404("discord integration is not available")
  903. redirect_uri = settings.SITE_ROOT + reverse("hc-add-discord")
  904. if "code" in request.GET:
  905. code = _get_validated_code(request, "discord")
  906. if code is None:
  907. return HttpResponseBadRequest()
  908. result = requests.post(
  909. "https://discordapp.com/api/oauth2/token",
  910. {
  911. "client_id": settings.DISCORD_CLIENT_ID,
  912. "client_secret": settings.DISCORD_CLIENT_SECRET,
  913. "code": code,
  914. "grant_type": "authorization_code",
  915. "redirect_uri": redirect_uri,
  916. },
  917. )
  918. doc = result.json()
  919. if "access_token" in doc:
  920. channel = Channel(kind="discord", project=request.project)
  921. channel.user = request.project.owner
  922. channel.value = result.text
  923. channel.save()
  924. channel.assign_all_checks()
  925. messages.success(request, "The Discord integration has been added!")
  926. else:
  927. messages.warning(request, "Something went wrong")
  928. return redirect("hc-channels")
  929. auth_url = "https://discordapp.com/api/oauth2/authorize?" + urlencode(
  930. {
  931. "client_id": settings.DISCORD_CLIENT_ID,
  932. "scope": "webhook.incoming",
  933. "redirect_uri": redirect_uri,
  934. "response_type": "code",
  935. "state": _prepare_state(request, "discord"),
  936. }
  937. )
  938. ctx = {"page": "channels", "project": request.project, "authorize_url": auth_url}
  939. return render(request, "integrations/add_discord.html", ctx)
  940. def add_pushover(request):
  941. if (
  942. settings.PUSHOVER_API_TOKEN is None
  943. or settings.PUSHOVER_SUBSCRIPTION_URL is None
  944. ):
  945. raise Http404("pushover integration is not available")
  946. if not request.user.is_authenticated:
  947. ctx = {"page": "channels"}
  948. return render(request, "integrations/add_pushover.html", ctx)
  949. if request.method == "POST":
  950. # Initiate the subscription
  951. state = _prepare_state(request, "pushover")
  952. failure_url = settings.SITE_ROOT + reverse("hc-channels")
  953. success_url = (
  954. settings.SITE_ROOT
  955. + reverse("hc-add-pushover")
  956. + "?"
  957. + urlencode(
  958. {
  959. "state": state,
  960. "prio": request.POST.get("po_priority", "0"),
  961. "prio_up": request.POST.get("po_priority_up", "0"),
  962. }
  963. )
  964. )
  965. subscription_url = (
  966. settings.PUSHOVER_SUBSCRIPTION_URL
  967. + "?"
  968. + urlencode({"success": success_url, "failure": failure_url})
  969. )
  970. return redirect(subscription_url)
  971. # Handle successful subscriptions
  972. if "pushover_user_key" in request.GET:
  973. key = _get_validated_code(request, "pushover", "pushover_user_key")
  974. if key is None:
  975. return HttpResponseBadRequest()
  976. # Validate priority
  977. prio = request.GET.get("prio")
  978. if prio not in ("-2", "-1", "0", "1", "2"):
  979. return HttpResponseBadRequest()
  980. prio_up = request.GET.get("prio_up")
  981. if prio_up not in ("-2", "-1", "0", "1", "2"):
  982. return HttpResponseBadRequest()
  983. if request.GET.get("pushover_unsubscribed") == "1":
  984. # Unsubscription: delete all Pushover channels for this project
  985. Channel.objects.filter(project=request.project, kind="po").delete()
  986. return redirect("hc-channels")
  987. # Subscription
  988. channel = Channel(project=request.project, kind="po")
  989. channel.value = "%s|%s|%s" % (key, prio, prio_up)
  990. channel.save()
  991. channel.assign_all_checks()
  992. messages.success(request, "The Pushover integration has been added!")
  993. return redirect("hc-channels")
  994. # Show Integration Settings form
  995. ctx = {
  996. "page": "channels",
  997. "project": request.project,
  998. "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY),
  999. "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION),
  1000. }
  1001. return render(request, "integrations/add_pushover.html", ctx)
  1002. @login_required
  1003. def add_opsgenie(request, code):
  1004. project = _get_project_for_user(request, code)
  1005. if request.method == "POST":
  1006. form = AddOpsGenieForm(request.POST)
  1007. if form.is_valid():
  1008. channel = Channel(project=project, kind="opsgenie")
  1009. v = {"region": form.cleaned_data["region"], "key": form.cleaned_data["key"]}
  1010. channel.value = json.dumps(v)
  1011. channel.save()
  1012. channel.assign_all_checks()
  1013. return redirect("hc-p-channels", project.code)
  1014. else:
  1015. form = AddOpsGenieForm()
  1016. ctx = {"page": "channels", "project": project, "form": form}
  1017. return render(request, "integrations/add_opsgenie.html", ctx)
  1018. @login_required
  1019. def add_victorops(request):
  1020. if request.method == "POST":
  1021. form = AddUrlForm(request.POST)
  1022. if form.is_valid():
  1023. channel = Channel(project=request.project, kind="victorops")
  1024. channel.value = form.cleaned_data["value"]
  1025. channel.save()
  1026. channel.assign_all_checks()
  1027. return redirect("hc-channels")
  1028. else:
  1029. form = AddUrlForm()
  1030. ctx = {"page": "channels", "project": request.project, "form": form}
  1031. return render(request, "integrations/add_victorops.html", ctx)
  1032. @csrf_exempt
  1033. @require_POST
  1034. def telegram_bot(request):
  1035. try:
  1036. doc = json.loads(request.body.decode())
  1037. jsonschema.validate(doc, telegram_callback)
  1038. except ValueError:
  1039. return HttpResponseBadRequest()
  1040. except jsonschema.ValidationError:
  1041. # We don't recognize the message format, but don't want Telegram
  1042. # retrying this over and over again, so respond with 200 OK
  1043. return HttpResponse()
  1044. if "/start" not in doc["message"]["text"]:
  1045. return HttpResponse()
  1046. chat = doc["message"]["chat"]
  1047. name = max(chat.get("title", ""), chat.get("username", ""))
  1048. invite = render_to_string(
  1049. "integrations/telegram_invite.html",
  1050. {"qs": signing.dumps((chat["id"], chat["type"], name))},
  1051. )
  1052. Telegram.send(chat["id"], invite)
  1053. return HttpResponse()
  1054. @login_required
  1055. def add_telegram(request):
  1056. chat_id, chat_type, chat_name = None, None, None
  1057. qs = request.META["QUERY_STRING"]
  1058. if qs:
  1059. chat_id, chat_type, chat_name = signing.loads(qs, max_age=600)
  1060. if request.method == "POST":
  1061. channel = Channel(project=request.project, kind="telegram")
  1062. channel.value = json.dumps(
  1063. {"id": chat_id, "type": chat_type, "name": chat_name}
  1064. )
  1065. channel.save()
  1066. channel.assign_all_checks()
  1067. messages.success(request, "The Telegram integration has been added!")
  1068. return redirect("hc-channels")
  1069. ctx = {
  1070. "page": "channels",
  1071. "project": request.project,
  1072. "chat_id": chat_id,
  1073. "chat_type": chat_type,
  1074. "chat_name": chat_name,
  1075. "bot_name": settings.TELEGRAM_BOT_NAME,
  1076. }
  1077. return render(request, "integrations/add_telegram.html", ctx)
  1078. @login_required
  1079. def add_sms(request, code):
  1080. if settings.TWILIO_AUTH is None:
  1081. raise Http404("sms integration is not available")
  1082. project = _get_project_for_user(request, code)
  1083. if request.method == "POST":
  1084. form = AddSmsForm(request.POST)
  1085. if form.is_valid():
  1086. channel = Channel(project=project, kind="sms")
  1087. channel.name = form.cleaned_data["label"]
  1088. channel.value = json.dumps({"value": form.cleaned_data["value"]})
  1089. channel.save()
  1090. channel.assign_all_checks()
  1091. return redirect("hc-p-channels", project.code)
  1092. else:
  1093. form = AddSmsForm()
  1094. ctx = {
  1095. "page": "channels",
  1096. "project": project,
  1097. "form": form,
  1098. "profile": project.owner_profile,
  1099. }
  1100. return render(request, "integrations/add_sms.html", ctx)
  1101. @login_required
  1102. def add_whatsapp(request):
  1103. if not settings.TWILIO_USE_WHATSAPP:
  1104. raise Http404("whatsapp integration is not available")
  1105. if request.method == "POST":
  1106. form = AddSmsForm(request.POST)
  1107. if form.is_valid():
  1108. channel = Channel(project=request.project, kind="whatsapp")
  1109. channel.name = form.cleaned_data["label"]
  1110. channel.value = json.dumps(
  1111. {
  1112. "value": form.cleaned_data["value"],
  1113. "up": form.cleaned_data["up"],
  1114. "down": form.cleaned_data["down"],
  1115. }
  1116. )
  1117. channel.save()
  1118. channel.assign_all_checks()
  1119. return redirect("hc-channels")
  1120. else:
  1121. form = AddSmsForm()
  1122. ctx = {
  1123. "page": "channels",
  1124. "project": request.project,
  1125. "form": form,
  1126. "profile": request.project.owner_profile,
  1127. }
  1128. return render(request, "integrations/add_whatsapp.html", ctx)
  1129. @login_required
  1130. def add_trello(request):
  1131. if settings.TRELLO_APP_KEY is None:
  1132. raise Http404("trello integration is not available")
  1133. if request.method == "POST":
  1134. channel = Channel(project=request.project, kind="trello")
  1135. channel.value = request.POST["settings"]
  1136. channel.save()
  1137. channel.assign_all_checks()
  1138. return redirect("hc-channels")
  1139. authorize_url = "https://trello.com/1/authorize?" + urlencode(
  1140. {
  1141. "expiration": "never",
  1142. "name": settings.SITE_NAME,
  1143. "scope": "read,write",
  1144. "response_type": "token",
  1145. "key": settings.TRELLO_APP_KEY,
  1146. "return_url": settings.SITE_ROOT + reverse("hc-add-trello"),
  1147. }
  1148. )
  1149. ctx = {
  1150. "page": "channels",
  1151. "project": request.project,
  1152. "authorize_url": authorize_url,
  1153. }
  1154. return render(request, "integrations/add_trello.html", ctx)
  1155. @login_required
  1156. def add_matrix(request, code):
  1157. if settings.MATRIX_ACCESS_TOKEN is None:
  1158. raise Http404("matrix integration is not available")
  1159. project = _get_project_for_user(request, code)
  1160. if request.method == "POST":
  1161. form = AddMatrixForm(request.POST)
  1162. if form.is_valid():
  1163. channel = Channel(project=project, kind="matrix")
  1164. channel.value = form.cleaned_data["room_id"]
  1165. # If user supplied room alias instead of ID, use it as channel name
  1166. alias = form.cleaned_data["alias"]
  1167. if not alias.startswith("!"):
  1168. channel.name = alias
  1169. channel.save()
  1170. channel.assign_all_checks()
  1171. messages.success(request, "The Matrix integration has been added!")
  1172. return redirect("hc-p-channels", project.code)
  1173. else:
  1174. form = AddMatrixForm()
  1175. ctx = {
  1176. "page": "channels",
  1177. "project": project,
  1178. "form": form,
  1179. "matrix_user_id": settings.MATRIX_USER_ID,
  1180. }
  1181. return render(request, "integrations/add_matrix.html", ctx)
  1182. @login_required
  1183. def add_apprise(request):
  1184. if not settings.APPRISE_ENABLED:
  1185. raise Http404("apprise integration is not available")
  1186. if request.method == "POST":
  1187. form = AddAppriseForm(request.POST)
  1188. if form.is_valid():
  1189. channel = Channel(project=request.project, kind="apprise")
  1190. channel.value = form.cleaned_data["url"]
  1191. channel.save()
  1192. channel.assign_all_checks()
  1193. messages.success(request, "The Apprise integration has been added!")
  1194. return redirect("hc-channels")
  1195. else:
  1196. form = AddAppriseForm()
  1197. ctx = {"page": "channels", "project": request.project, "form": form}
  1198. return render(request, "integrations/add_apprise.html", ctx)
  1199. @login_required
  1200. @require_POST
  1201. def trello_settings(request):
  1202. token = request.POST.get("token")
  1203. url = "https://api.trello.com/1/members/me/boards?" + urlencode(
  1204. {
  1205. "key": settings.TRELLO_APP_KEY,
  1206. "token": token,
  1207. "fields": "id,name",
  1208. "lists": "open",
  1209. "list_fields": "id,name",
  1210. }
  1211. )
  1212. r = requests.get(url)
  1213. ctx = {"token": token, "data": r.json()}
  1214. return render(request, "integrations/trello_settings.html", ctx)
  1215. @login_required
  1216. def add_msteams(request, code):
  1217. project = _get_project_for_user(request, code)
  1218. if request.method == "POST":
  1219. form = AddUrlForm(request.POST)
  1220. if form.is_valid():
  1221. channel = Channel(project=project, kind="msteams")
  1222. channel.value = form.cleaned_data["value"]
  1223. channel.save()
  1224. channel.assign_all_checks()
  1225. return redirect("hc-p-channels", project.code)
  1226. else:
  1227. form = AddUrlForm()
  1228. ctx = {"page": "channels", "project": project, "form": form}
  1229. return render(request, "integrations/add_msteams.html", ctx)
  1230. @login_required
  1231. def add_prometheus(request, code):
  1232. project = _get_project_for_user(request, code)
  1233. ctx = {"page": "channels", "project": project}
  1234. return render(request, "integrations/add_prometheus.html", ctx)
  1235. def metrics(request, code, key):
  1236. if len(key) != 32:
  1237. return HttpResponseBadRequest()
  1238. q = Project.objects.filter(code=code, api_key_readonly=key)
  1239. try:
  1240. project = q.get()
  1241. except Project.DoesNotExist:
  1242. return HttpResponseForbidden()
  1243. checks = Check.objects.filter(project_id=project.id).order_by("id")
  1244. def esc(s):
  1245. return s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
  1246. def output(checks):
  1247. yield "# HELP hc_check_up Whether the check is currently up (1 for yes, 0 for no).\n"
  1248. yield "# TYPE hc_check_up gauge\n"
  1249. TMPL = """hc_check_up{name="%s", tags="%s", unique_key="%s"} %d\n"""
  1250. for check in checks:
  1251. value = 0 if check.get_status(with_started=False) == "down" else 1
  1252. yield TMPL % (esc(check.name), esc(check.tags), check.unique_key, value)
  1253. tags_statuses, num_down = _tags_statuses(checks)
  1254. yield "\n"
  1255. yield "# HELP hc_tag_up Whether all checks with this tag are up (1 for yes, 0 for no).\n"
  1256. yield "# TYPE hc_tag_up gauge\n"
  1257. TMPL = """hc_tag_up{tag="%s"} %d\n"""
  1258. for tag in sorted(tags_statuses):
  1259. value = 0 if tags_statuses[tag] == "down" else 1
  1260. yield TMPL % (esc(tag), value)
  1261. yield "\n"
  1262. yield "# HELP hc_checks_total The total number of checks.\n"
  1263. yield "# TYPE hc_checks_total gauge\n"
  1264. yield "hc_checks_total %d\n" % len(checks)
  1265. yield "\n"
  1266. yield "# HELP hc_checks_down_total The number of checks currently down.\n"
  1267. yield "# TYPE hc_checks_down_total gauge\n"
  1268. yield "hc_checks_down_total %d\n" % num_down
  1269. return HttpResponse(output(checks), content_type="text/plain")