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.

742 lines
22 KiB

9 years ago
8 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
9 years ago
8 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
8 years ago
  1. from collections import Counter
  2. from croniter import croniter
  3. from datetime import datetime, timedelta as td
  4. from itertools import tee
  5. import requests
  6. from django.conf import settings
  7. from django.contrib import messages
  8. from django.contrib.auth.decorators import login_required
  9. from django.db.models import Count
  10. from django.http import Http404, HttpResponseBadRequest, HttpResponseForbidden
  11. from django.shortcuts import get_object_or_404, redirect, render
  12. from django.urls import reverse
  13. from django.utils import timezone
  14. from django.utils.crypto import get_random_string
  15. from django.views.decorators.csrf import csrf_exempt
  16. from django.utils.six.moves.urllib.parse import urlencode
  17. from hc.api.decorators import uuid_or_400
  18. from hc.api.models import (DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check,
  19. Ping, Notification)
  20. from hc.front.forms import (AddWebhookForm, NameTagsForm,
  21. TimeoutForm, AddUrlForm, AddPdForm, AddEmailForm,
  22. AddOpsGenieForm, CronForm)
  23. from pytz import all_timezones
  24. from pytz.exceptions import UnknownTimeZoneError
  25. # from itertools recipes:
  26. def pairwise(iterable):
  27. "s -> (s0,s1), (s1,s2), (s2, s3), ..."
  28. a, b = tee(iterable)
  29. next(b, None)
  30. return zip(a, b)
  31. @login_required
  32. def my_checks(request):
  33. q = Check.objects.filter(user=request.team.user).order_by("created")
  34. checks = list(q)
  35. counter = Counter()
  36. down_tags, grace_tags = set(), set()
  37. for check in checks:
  38. status = check.get_status()
  39. for tag in check.tags_list():
  40. if tag == "":
  41. continue
  42. counter[tag] += 1
  43. if status == "down":
  44. down_tags.add(tag)
  45. elif check.in_grace_period():
  46. grace_tags.add(tag)
  47. ctx = {
  48. "page": "checks",
  49. "checks": checks,
  50. "now": timezone.now(),
  51. "tags": counter.most_common(),
  52. "down_tags": down_tags,
  53. "grace_tags": grace_tags,
  54. "ping_endpoint": settings.PING_ENDPOINT,
  55. "timezones": all_timezones
  56. }
  57. return render(request, "front/my_checks.html", ctx)
  58. def _welcome_check(request):
  59. check = None
  60. if "welcome_code" in request.session:
  61. code = request.session["welcome_code"]
  62. check = Check.objects.filter(code=code).first()
  63. if check is None:
  64. check = Check()
  65. check.save()
  66. request.session["welcome_code"] = str(check.code)
  67. return check
  68. def index(request):
  69. if request.user.is_authenticated:
  70. return redirect("hc-checks")
  71. check = _welcome_check(request)
  72. ctx = {
  73. "page": "welcome",
  74. "check": check,
  75. "ping_url": check.url(),
  76. "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None,
  77. "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
  78. "enable_discord": settings.DISCORD_CLIENT_ID is not None
  79. }
  80. return render(request, "front/welcome.html", ctx)
  81. def docs(request):
  82. check = _welcome_check(request)
  83. ctx = {
  84. "page": "docs",
  85. "section": "home",
  86. "ping_endpoint": settings.PING_ENDPOINT,
  87. "check": check,
  88. "ping_url": check.url()
  89. }
  90. return render(request, "front/docs.html", ctx)
  91. def docs_api(request):
  92. ctx = {
  93. "page": "docs",
  94. "section": "api",
  95. "SITE_ROOT": settings.SITE_ROOT,
  96. "PING_ENDPOINT": settings.PING_ENDPOINT,
  97. "default_timeout": int(DEFAULT_TIMEOUT.total_seconds()),
  98. "default_grace": int(DEFAULT_GRACE.total_seconds())
  99. }
  100. return render(request, "front/docs_api.html", ctx)
  101. def about(request):
  102. return render(request, "front/about.html", {"page": "about"})
  103. @login_required
  104. def add_check(request):
  105. if request.method != "POST":
  106. return HttpResponseBadRequest()
  107. check = Check(user=request.team.user)
  108. check.save()
  109. check.assign_all_channels()
  110. return redirect("hc-checks")
  111. @login_required
  112. @uuid_or_400
  113. def update_name(request, code):
  114. if request.method != "POST":
  115. return HttpResponseBadRequest()
  116. check = get_object_or_404(Check, code=code)
  117. if check.user_id != request.team.user.id:
  118. return HttpResponseForbidden()
  119. form = NameTagsForm(request.POST)
  120. if form.is_valid():
  121. check.name = form.cleaned_data["name"]
  122. check.tags = form.cleaned_data["tags"]
  123. check.save()
  124. return redirect("hc-checks")
  125. @login_required
  126. @uuid_or_400
  127. def update_timeout(request, code):
  128. if request.method != "POST":
  129. return HttpResponseBadRequest()
  130. check = get_object_or_404(Check, code=code)
  131. if check.user != request.team.user:
  132. return HttpResponseForbidden()
  133. kind = request.POST.get("kind")
  134. if kind == "simple":
  135. form = TimeoutForm(request.POST)
  136. if not form.is_valid():
  137. return HttpResponseBadRequest()
  138. check.kind = "simple"
  139. check.timeout = td(seconds=form.cleaned_data["timeout"])
  140. check.grace = td(seconds=form.cleaned_data["grace"])
  141. elif kind == "cron":
  142. form = CronForm(request.POST)
  143. if not form.is_valid():
  144. return HttpResponseBadRequest()
  145. check.kind = "cron"
  146. check.schedule = form.cleaned_data["schedule"]
  147. check.tz = form.cleaned_data["tz"]
  148. check.grace = td(minutes=form.cleaned_data["grace"])
  149. if check.last_ping:
  150. check.alert_after = check.get_alert_after()
  151. check.save()
  152. return redirect("hc-checks")
  153. @csrf_exempt
  154. def cron_preview(request):
  155. if request.method != "POST":
  156. return HttpResponseBadRequest()
  157. schedule = request.POST.get("schedule")
  158. tz = request.POST.get("tz")
  159. ctx = {"tz": tz, "dates": []}
  160. try:
  161. with timezone.override(tz):
  162. now_naive = timezone.make_naive(timezone.now())
  163. it = croniter(schedule, now_naive)
  164. for i in range(0, 6):
  165. naive = it.get_next(datetime)
  166. aware = timezone.make_aware(naive)
  167. ctx["dates"].append((naive, aware))
  168. except UnknownTimeZoneError:
  169. ctx["bad_tz"] = True
  170. except:
  171. ctx["bad_schedule"] = True
  172. return render(request, "front/cron_preview.html", ctx)
  173. @login_required
  174. @uuid_or_400
  175. def pause(request, code):
  176. if request.method != "POST":
  177. return HttpResponseBadRequest()
  178. check = get_object_or_404(Check, code=code)
  179. if check.user_id != request.team.user.id:
  180. return HttpResponseForbidden()
  181. check.status = "paused"
  182. check.save()
  183. return redirect("hc-checks")
  184. @login_required
  185. @uuid_or_400
  186. def remove_check(request, code):
  187. if request.method != "POST":
  188. return HttpResponseBadRequest()
  189. check = get_object_or_404(Check, code=code)
  190. if check.user != request.team.user:
  191. return HttpResponseForbidden()
  192. check.delete()
  193. return redirect("hc-checks")
  194. @login_required
  195. @uuid_or_400
  196. def log(request, code):
  197. check = get_object_or_404(Check, code=code)
  198. if check.user != request.team.user:
  199. return HttpResponseForbidden()
  200. limit = request.team.ping_log_limit
  201. pings = Ping.objects.filter(owner=check).order_by("-id")[:limit + 1]
  202. pings = list(pings)
  203. num_pings = len(pings)
  204. pings = pings[:limit]
  205. alerts = []
  206. if len(pings):
  207. cutoff = pings[-1].created
  208. alerts = Notification.objects \
  209. .select_related("channel") \
  210. .filter(owner=check, check_status="down", created__gt=cutoff)
  211. events = pings + list(alerts)
  212. events.sort(key=lambda el: el.created, reverse=True)
  213. ctx = {
  214. "check": check,
  215. "events": events,
  216. "num_pings": min(num_pings, limit),
  217. "limit": limit,
  218. "show_limit_notice": num_pings > limit and settings.USE_PAYMENTS
  219. }
  220. return render(request, "front/log.html", ctx)
  221. @login_required
  222. def channels(request):
  223. if request.method == "POST":
  224. code = request.POST["channel"]
  225. try:
  226. channel = Channel.objects.get(code=code)
  227. except Channel.DoesNotExist:
  228. return HttpResponseBadRequest()
  229. if channel.user_id != request.team.user.id:
  230. return HttpResponseForbidden()
  231. new_checks = []
  232. for key in request.POST:
  233. if key.startswith("check-"):
  234. code = key[6:]
  235. try:
  236. check = Check.objects.get(code=code)
  237. except Check.DoesNotExist:
  238. return HttpResponseBadRequest()
  239. if check.user_id != request.team.user.id:
  240. return HttpResponseForbidden()
  241. new_checks.append(check)
  242. channel.checks = new_checks
  243. return redirect("hc-channels")
  244. channels = Channel.objects.filter(user=request.team.user)
  245. channels = channels.order_by("created")
  246. channels = channels.annotate(n_checks=Count("checks"))
  247. num_checks = Check.objects.filter(user=request.team.user).count()
  248. ctx = {
  249. "page": "channels",
  250. "channels": channels,
  251. "num_checks": num_checks,
  252. "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None,
  253. "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
  254. "enable_discord": settings.DISCORD_CLIENT_ID is not None
  255. }
  256. return render(request, "front/channels.html", ctx)
  257. @login_required
  258. @uuid_or_400
  259. def channel_checks(request, code):
  260. channel = get_object_or_404(Channel, code=code)
  261. if channel.user_id != request.team.user.id:
  262. return HttpResponseForbidden()
  263. assigned = set(channel.checks.values_list('code', flat=True).distinct())
  264. checks = Check.objects.filter(user=request.team.user).order_by("created")
  265. ctx = {
  266. "checks": checks,
  267. "assigned": assigned,
  268. "channel": channel
  269. }
  270. return render(request, "front/channel_checks.html", ctx)
  271. @uuid_or_400
  272. def verify_email(request, code, token):
  273. channel = get_object_or_404(Channel, code=code)
  274. if channel.make_token() == token:
  275. channel.email_verified = True
  276. channel.save()
  277. return render(request, "front/verify_email_success.html")
  278. return render(request, "bad_link.html")
  279. @uuid_or_400
  280. def unsubscribe_email(request, code, token):
  281. channel = get_object_or_404(Channel, code=code)
  282. if channel.make_token() != token:
  283. return render(request, "bad_link.html")
  284. if channel.kind != "email":
  285. return HttpResponseBadRequest()
  286. channel.delete()
  287. return render(request, "front/unsubscribe_success.html")
  288. @login_required
  289. @uuid_or_400
  290. def remove_channel(request, code):
  291. if request.method != "POST":
  292. return HttpResponseBadRequest()
  293. # user may refresh the page during POST and cause two deletion attempts
  294. channel = Channel.objects.filter(code=code).first()
  295. if channel:
  296. if channel.user != request.team.user:
  297. return HttpResponseForbidden()
  298. channel.delete()
  299. return redirect("hc-channels")
  300. @login_required
  301. def add_email(request):
  302. if request.method == "POST":
  303. form = AddEmailForm(request.POST)
  304. if form.is_valid():
  305. channel = Channel(user=request.team.user, kind="email")
  306. channel.value = form.cleaned_data["value"]
  307. channel.save()
  308. channel.assign_all_checks()
  309. channel.send_verify_link()
  310. return redirect("hc-channels")
  311. else:
  312. form = AddEmailForm()
  313. ctx = {"page": "channels", "form": form}
  314. return render(request, "integrations/add_email.html", ctx)
  315. @login_required
  316. def add_webhook(request):
  317. if request.method == "POST":
  318. form = AddWebhookForm(request.POST)
  319. if form.is_valid():
  320. channel = Channel(user=request.team.user, kind="webhook")
  321. channel.value = form.get_value()
  322. channel.save()
  323. channel.assign_all_checks()
  324. return redirect("hc-channels")
  325. else:
  326. form = AddWebhookForm()
  327. ctx = {
  328. "page": "channels",
  329. "form": form,
  330. "now": timezone.now().replace(microsecond=0).isoformat()
  331. }
  332. return render(request, "integrations/add_webhook.html", ctx)
  333. @login_required
  334. def add_pd(request):
  335. if request.method == "POST":
  336. form = AddPdForm(request.POST)
  337. if form.is_valid():
  338. channel = Channel(user=request.team.user, kind="pd")
  339. channel.value = form.cleaned_data["value"]
  340. channel.save()
  341. channel.assign_all_checks()
  342. return redirect("hc-channels")
  343. else:
  344. form = AddPdForm()
  345. ctx = {"page": "channels", "form": form}
  346. return render(request, "integrations/add_pd.html", ctx)
  347. def _prepare_state(request, session_key):
  348. state = get_random_string()
  349. request.session[session_key] = state
  350. return state
  351. def _get_validated_code(request, session_key):
  352. if session_key not in request.session:
  353. return None
  354. session_state = request.session.pop(session_key)
  355. request_state = request.GET.get("state")
  356. if session_state is None or session_state != request_state:
  357. return None
  358. return request.GET.get("code")
  359. def add_slack(request):
  360. if not settings.SLACK_CLIENT_ID and not request.user.is_authenticated:
  361. return redirect("hc-login")
  362. if request.method == "POST":
  363. form = AddUrlForm(request.POST)
  364. if form.is_valid():
  365. channel = Channel(user=request.team.user, kind="slack")
  366. channel.value = form.cleaned_data["value"]
  367. channel.save()
  368. channel.assign_all_checks()
  369. return redirect("hc-channels")
  370. else:
  371. form = AddUrlForm()
  372. ctx = {
  373. "page": "channels",
  374. "form": form,
  375. "slack_client_id": settings.SLACK_CLIENT_ID
  376. }
  377. if settings.SLACK_CLIENT_ID:
  378. ctx["state"] = _prepare_state(request, "slack")
  379. return render(request, "integrations/add_slack.html", ctx)
  380. @login_required
  381. def add_slack_btn(request):
  382. code = _get_validated_code(request, "slack")
  383. if code is None:
  384. return HttpResponseBadRequest()
  385. result = requests.post("https://slack.com/api/oauth.access", {
  386. "client_id": settings.SLACK_CLIENT_ID,
  387. "client_secret": settings.SLACK_CLIENT_SECRET,
  388. "code": code
  389. })
  390. doc = result.json()
  391. if doc.get("ok"):
  392. channel = Channel()
  393. channel.user = request.team.user
  394. channel.kind = "slack"
  395. channel.value = result.text
  396. channel.save()
  397. channel.assign_all_checks()
  398. messages.success(request, "The Slack integration has been added!")
  399. else:
  400. s = doc.get("error")
  401. messages.warning(request, "Error message from slack: %s" % s)
  402. return redirect("hc-channels")
  403. @login_required
  404. def add_hipchat(request):
  405. if request.method == "POST":
  406. form = AddUrlForm(request.POST)
  407. if form.is_valid():
  408. channel = Channel(user=request.team.user, kind="hipchat")
  409. channel.value = form.cleaned_data["value"]
  410. channel.save()
  411. channel.assign_all_checks()
  412. return redirect("hc-channels")
  413. else:
  414. form = AddUrlForm()
  415. ctx = {"page": "channels", "form": form}
  416. return render(request, "integrations/add_hipchat.html", ctx)
  417. @login_required
  418. def add_pushbullet(request):
  419. if settings.PUSHBULLET_CLIENT_ID is None:
  420. raise Http404("pushbullet integration is not available")
  421. if "code" in request.GET:
  422. code = _get_validated_code(request, "pushbullet")
  423. if code is None:
  424. return HttpResponseBadRequest()
  425. result = requests.post("https://api.pushbullet.com/oauth2/token", {
  426. "client_id": settings.PUSHBULLET_CLIENT_ID,
  427. "client_secret": settings.PUSHBULLET_CLIENT_SECRET,
  428. "code": code,
  429. "grant_type": "authorization_code"
  430. })
  431. doc = result.json()
  432. if "access_token" in doc:
  433. channel = Channel(kind="pushbullet")
  434. channel.user = request.team.user
  435. channel.value = doc["access_token"]
  436. channel.save()
  437. channel.assign_all_checks()
  438. messages.success(request,
  439. "The Pushbullet integration has been added!")
  440. else:
  441. messages.warning(request, "Something went wrong")
  442. return redirect("hc-channels")
  443. redirect_uri = settings.SITE_ROOT + reverse("hc-add-pushbullet")
  444. authorize_url = "https://www.pushbullet.com/authorize?" + urlencode({
  445. "client_id": settings.PUSHBULLET_CLIENT_ID,
  446. "redirect_uri": redirect_uri,
  447. "response_type": "code",
  448. "state": _prepare_state(request, "pushbullet")
  449. })
  450. ctx = {
  451. "page": "channels",
  452. "authorize_url": authorize_url
  453. }
  454. return render(request, "integrations/add_pushbullet.html", ctx)
  455. @login_required
  456. def add_discord(request):
  457. if settings.DISCORD_CLIENT_ID is None:
  458. raise Http404("discord integration is not available")
  459. redirect_uri = settings.SITE_ROOT + reverse("hc-add-discord")
  460. if "code" in request.GET:
  461. code = _get_validated_code(request, "discord")
  462. if code is None:
  463. return HttpResponseBadRequest()
  464. result = requests.post("https://discordapp.com/api/oauth2/token", {
  465. "client_id": settings.DISCORD_CLIENT_ID,
  466. "client_secret": settings.DISCORD_CLIENT_SECRET,
  467. "code": code,
  468. "grant_type": "authorization_code",
  469. "redirect_uri": redirect_uri
  470. })
  471. doc = result.json()
  472. if "access_token" in doc:
  473. channel = Channel(kind="discord")
  474. channel.user = request.team.user
  475. channel.value = result.text
  476. channel.save()
  477. channel.assign_all_checks()
  478. messages.success(request,
  479. "The Discord integration has been added!")
  480. else:
  481. messages.warning(request, "Something went wrong")
  482. return redirect("hc-channels")
  483. auth_url = "https://discordapp.com/api/oauth2/authorize?" + urlencode({
  484. "client_id": settings.DISCORD_CLIENT_ID,
  485. "scope": "webhook.incoming",
  486. "redirect_uri": redirect_uri,
  487. "response_type": "code",
  488. "state": _prepare_state(request, "discord")
  489. })
  490. ctx = {
  491. "page": "channels",
  492. "authorize_url": auth_url
  493. }
  494. return render(request, "integrations/add_discord.html", ctx)
  495. @login_required
  496. def add_pushover(request):
  497. if settings.PUSHOVER_API_TOKEN is None or settings.PUSHOVER_SUBSCRIPTION_URL is None:
  498. raise Http404("pushover integration is not available")
  499. if request.method == "POST":
  500. # Initiate the subscription
  501. nonce = get_random_string()
  502. request.session["po_nonce"] = nonce
  503. failure_url = settings.SITE_ROOT + reverse("hc-channels")
  504. success_url = settings.SITE_ROOT + reverse("hc-add-pushover") + "?" + urlencode({
  505. "nonce": nonce,
  506. "prio": request.POST.get("po_priority", "0"),
  507. })
  508. subscription_url = settings.PUSHOVER_SUBSCRIPTION_URL + "?" + urlencode({
  509. "success": success_url,
  510. "failure": failure_url,
  511. })
  512. return redirect(subscription_url)
  513. # Handle successful subscriptions
  514. if "pushover_user_key" in request.GET:
  515. if "nonce" not in request.GET or "prio" not in request.GET:
  516. return HttpResponseBadRequest()
  517. # Validate nonce
  518. if request.GET["nonce"] != request.session.get("po_nonce"):
  519. return HttpResponseForbidden()
  520. # Validate priority
  521. if request.GET["prio"] not in ("-2", "-1", "0", "1", "2"):
  522. return HttpResponseBadRequest()
  523. # All looks well--
  524. del request.session["po_nonce"]
  525. if request.GET.get("pushover_unsubscribed") == "1":
  526. # Unsubscription: delete all Pushover channels for this user
  527. Channel.objects.filter(user=request.user, kind="po").delete()
  528. return redirect("hc-channels")
  529. else:
  530. # Subscription
  531. user_key = request.GET["pushover_user_key"]
  532. priority = int(request.GET["prio"])
  533. channel = Channel(user=request.team.user, kind="po")
  534. channel.value = "%s|%d" % (user_key, priority)
  535. channel.save()
  536. channel.assign_all_checks()
  537. return redirect("hc-channels")
  538. # Show Integration Settings form
  539. ctx = {
  540. "page": "channels",
  541. "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY),
  542. "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION),
  543. }
  544. return render(request, "integrations/add_pushover.html", ctx)
  545. @login_required
  546. def add_opsgenie(request):
  547. if request.method == "POST":
  548. form = AddOpsGenieForm(request.POST)
  549. if form.is_valid():
  550. channel = Channel(user=request.team.user, kind="opsgenie")
  551. channel.value = form.cleaned_data["value"]
  552. channel.save()
  553. channel.assign_all_checks()
  554. return redirect("hc-channels")
  555. else:
  556. form = AddUrlForm()
  557. ctx = {"page": "channels", "form": form}
  558. return render(request, "integrations/add_opsgenie.html", ctx)
  559. @login_required
  560. def add_victorops(request):
  561. if request.method == "POST":
  562. form = AddUrlForm(request.POST)
  563. if form.is_valid():
  564. channel = Channel(user=request.team.user, kind="victorops")
  565. channel.value = form.cleaned_data["value"]
  566. channel.save()
  567. channel.assign_all_checks()
  568. return redirect("hc-channels")
  569. else:
  570. form = AddUrlForm()
  571. ctx = {"page": "channels", "form": form}
  572. return render(request, "integrations/add_victorops.html", ctx)
  573. def privacy(request):
  574. return render(request, "front/privacy.html", {})
  575. def terms(request):
  576. return render(request, "front/terms.html", {})