/ cabot / cabotapp / views.py
views.py
   1  import json
   2  import re
   3  from datetime import date, datetime, timedelta
   4  from itertools import dropwhile, groupby, izip_longest
   5  
   6  import requests
   7  from alert import AlertPlugin, AlertPluginUserData
   8  from cabot.cabotapp import alert
   9  from cabot.cabotapp.utils import cabot_needs_setup
  10  from dateutil.relativedelta import relativedelta
  11  from django import forms
  12  from django.conf import settings
  13  from django.contrib import messages
  14  from django.contrib.auth import get_user_model
  15  from django.contrib.auth.decorators import login_required
  16  from django.contrib.auth.models import User
  17  from django.core.exceptions import ValidationError
  18  from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
  19  from django.core.urlresolvers import reverse, reverse_lazy
  20  from django.core.validators import URLValidator
  21  from django.db import transaction
  22  from django.db.models.functions import Lower
  23  from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
  24  from django.shortcuts import redirect, render
  25  from django.template import RequestContext, loader
  26  from django.utils import timezone
  27  from django.utils.decorators import method_decorator
  28  from django.utils.timezone import utc
  29  from django.views.generic import (CreateView, DeleteView, DetailView, ListView,
  30                                    TemplateView, UpdateView, View)
  31  from models import (GraphiteStatusCheck, HttpStatusCheck, ICMPStatusCheck,
  32                      Instance, JenkinsStatusCheck, Service, Shift, StatusCheck,
  33                      StatusCheckResult, UserProfile, get_custom_check_plugins,
  34                      get_duty_officers)
  35  from rest_framework.views import APIView
  36  from rest_framework.response import Response
  37  from tasks import run_status_check as _run_status_check
  38  
  39  from .graphite import get_data, get_matching_metrics
  40  
  41  
  42  class LoginRequiredMixin(object):
  43      @method_decorator(login_required)
  44      def dispatch(self, *args, **kwargs):
  45          return super(LoginRequiredMixin, self).dispatch(*args, **kwargs)
  46  
  47  
  48  @login_required
  49  def subscriptions(request):
  50      """ Simple list of all checks """
  51      services = Service.objects.all()
  52      users = User.objects.filter(is_active=True)
  53  
  54      return render(request, 'cabotapp/subscriptions.html', {
  55          'services': services,
  56          'users': users,
  57          'duty_officers': get_duty_officers(),
  58          'custom_check_types': get_custom_check_plugins(),
  59      })
  60  
  61  
  62  @login_required
  63  def run_status_check(request, pk):
  64      """Runs a specific check"""
  65      _run_status_check(check_or_id=pk)
  66      return HttpResponseRedirect(reverse('check', kwargs={'pk': pk}))
  67  
  68  
  69  def duplicate_icmp_check(request, pk):
  70      pc = StatusCheck.objects.get(pk=pk)
  71      npk = pc.duplicate()
  72      return HttpResponseRedirect(reverse('update-icmp-check', kwargs={'pk': npk}))
  73  
  74  
  75  def duplicate_instance(request, pk):
  76      instance = Instance.objects.get(pk=pk)
  77      new_instance = instance.duplicate()
  78      return HttpResponseRedirect(reverse('update-instance', kwargs={'pk': new_instance}))
  79  
  80  
  81  def duplicate_http_check(request, pk):
  82      pc = StatusCheck.objects.get(pk=pk)
  83      npk = pc.duplicate()
  84      return HttpResponseRedirect(reverse('update-http-check', kwargs={'pk': npk}))
  85  
  86  
  87  def duplicate_graphite_check(request, pk):
  88      pc = StatusCheck.objects.get(pk=pk)
  89      npk = pc.duplicate()
  90      return HttpResponseRedirect(reverse('update-graphite-check', kwargs={'pk': npk}))
  91  
  92  
  93  def duplicate_jenkins_check(request, pk):
  94      pc = StatusCheck.objects.get(pk=pk)
  95      npk = pc.duplicate()
  96      return HttpResponseRedirect(reverse('update-jenkins-check', kwargs={'pk': npk}))
  97  
  98  
  99  class BaseCommonView(object):
 100      def render_to_response(self, context, *args, **kwargs):
 101          if context is None:
 102              context = {}
 103          context['custom_check_types'] = get_custom_check_plugins()
 104          return super(BaseCommonView, self).render_to_response(context, *args, **kwargs)
 105  
 106  
 107  class CommonCreateView(BaseCommonView, CreateView):
 108      pass
 109  
 110  
 111  class CommonUpdateView(BaseCommonView, UpdateView):
 112      pass
 113  
 114  
 115  class CommonDeleteView(BaseCommonView, DeleteView):
 116      pass
 117  
 118  
 119  class CommonDetailView(BaseCommonView, DetailView):
 120      pass
 121  
 122  
 123  class CommonListView(BaseCommonView, ListView):
 124      pass
 125  
 126  
 127  class StatusCheckResultDetailView(LoginRequiredMixin, CommonDetailView):
 128      model = StatusCheckResult
 129      context_object_name = 'result'
 130  
 131  
 132  class SymmetricalForm(forms.ModelForm):
 133      symmetrical_fields = ()  # Iterable of 2-tuples (field, model)
 134  
 135      def __init__(self, *args, **kwargs):
 136          super(SymmetricalForm, self).__init__(*args, **kwargs)
 137  
 138          if self.instance and self.instance.pk:
 139              for field in self.symmetrical_fields:
 140                  self.fields[field].initial = getattr(
 141                      self.instance, field).all()
 142  
 143      def save(self, commit=True):
 144          instance = super(SymmetricalForm, self).save(commit=False)
 145          if commit:
 146              instance.save()
 147          if instance.pk:
 148              for field in self.symmetrical_fields:
 149                  setattr(instance, field, self.cleaned_data[field])
 150              self.save_m2m()
 151          return instance
 152  
 153  
 154  base_widgets = {
 155      'name': forms.TextInput(attrs={
 156          'style': 'width:30%',
 157      }),
 158      'importance': forms.RadioSelect(),
 159  }
 160  
 161  
 162  class StatusCheckForm(SymmetricalForm):
 163      symmetrical_fields = ('service_set', 'instance_set')
 164  
 165      service_set = forms.ModelMultipleChoiceField(
 166          queryset=Service.objects.all(),
 167          required=False,
 168          help_text='Link to service(s).',
 169          widget=forms.SelectMultiple(
 170              attrs={
 171                  'data-rel': 'chosen',
 172                  'style': 'width: 70%',
 173              },
 174          )
 175      )
 176  
 177      instance_set = forms.ModelMultipleChoiceField(
 178          queryset=Instance.objects.all(),
 179          required=False,
 180          help_text='Link to instance(s).',
 181          widget=forms.SelectMultiple(
 182              attrs={
 183                  'data-rel': 'chosen',
 184                  'style': 'width: 70%',
 185              },
 186          )
 187      )
 188  
 189  
 190  class GraphiteStatusCheckForm(StatusCheckForm):
 191      class Meta:
 192          model = GraphiteStatusCheck
 193          fields = (
 194              'name',
 195              'metric',
 196              'check_type',
 197              'value',
 198              'frequency',
 199              'active',
 200              'importance',
 201              'expected_num_hosts',
 202              'allowed_num_failures',
 203              'debounce',
 204          )
 205          widgets = dict(**base_widgets)
 206          widgets.update({
 207              'value': forms.TextInput(attrs={
 208                  'style': 'width: 100px',
 209                  'placeholder': 'threshold value',
 210              }),
 211              'metric': forms.TextInput(attrs={
 212                  'style': 'width: 100%',
 213                  'placeholder': 'graphite metric key'
 214              }),
 215              'check_type': forms.Select(attrs={
 216                  'data-rel': 'chosen',
 217              })
 218          })
 219  
 220  
 221  class ICMPStatusCheckForm(StatusCheckForm):
 222      class Meta:
 223          model = ICMPStatusCheck
 224          fields = (
 225              'name',
 226              'frequency',
 227              'importance',
 228              'active',
 229              'debounce',
 230          )
 231          widgets = dict(**base_widgets)
 232  
 233  
 234  class HttpStatusCheckForm(StatusCheckForm):
 235      class Meta:
 236          model = HttpStatusCheck
 237          fields = (
 238              'name',
 239              'endpoint',
 240              'username',
 241              'password',
 242              'text_match',
 243              'status_code',
 244              'timeout',
 245              'verify_ssl_certificate',
 246              'frequency',
 247              'importance',
 248              'active',
 249              'debounce',
 250          )
 251          widgets = dict(**base_widgets)
 252          widgets.update({
 253              'endpoint': forms.TextInput(attrs={
 254                  'style': 'width: 100%',
 255                  'placeholder': 'https://www.example.org/',
 256              }),
 257              'username': forms.TextInput(attrs={
 258                  'style': 'width: 30%',
 259              }),
 260              'password': forms.PasswordInput(attrs={
 261                  'style': 'width: 30%',
 262                  # Prevent auto-fill with saved Cabot log-in credentials:
 263                  'autocomplete': 'new-password',
 264              }),
 265              'text_match': forms.TextInput(attrs={
 266                  'style': 'width: 100%',
 267                  'placeholder': '[Aa]rachnys\s+[Rr]ules',
 268              }),
 269              'status_code': forms.TextInput(attrs={
 270                  'style': 'width: 20%',
 271                  'placeholder': '200',
 272              }),
 273          })
 274  
 275      def clean_password(self):
 276          new_password_value = self.cleaned_data['password']
 277          if new_password_value == '':
 278              new_password_value = self.initial.get('password')
 279          return new_password_value
 280  
 281  
 282  class JenkinsStatusCheckForm(StatusCheckForm):
 283      class Meta:
 284          model = JenkinsStatusCheck
 285          fields = (
 286              'name',
 287              'importance',
 288              'debounce',
 289              'max_queued_build_time',
 290              'jenkins_config',
 291          )
 292          widgets = dict(**base_widgets)
 293  
 294  
 295  class InstanceForm(SymmetricalForm):
 296      symmetrical_fields = ('service_set',)
 297      service_set = forms.ModelMultipleChoiceField(
 298          queryset=Service.objects.all(),
 299          required=False,
 300          help_text='Link to service(s).',
 301          widget=forms.SelectMultiple(
 302              attrs={
 303                  'data-rel': 'chosen',
 304                  'style': 'width: 70%',
 305              },
 306          )
 307      )
 308  
 309      class Meta:
 310          model = Instance
 311          template_name = 'instance_form.html'
 312          fields = (
 313              'name',
 314              'address',
 315              'users_to_notify',
 316              'status_checks',
 317              'service_set',
 318          )
 319          widgets = {
 320              'name': forms.TextInput(attrs={'style': 'width: 70%;'}),
 321              'address': forms.TextInput(attrs={'style': 'width: 70%;'}),
 322              'status_checks': forms.SelectMultiple(attrs={
 323                  'data-rel': 'chosen',
 324                  'style': 'width: 70%',
 325              }),
 326              'service_set': forms.SelectMultiple(attrs={
 327                  'data-rel': 'chosen',
 328                  'style': 'width: 70%',
 329              }),
 330              'alerts': forms.SelectMultiple(attrs={
 331                  'data-rel': 'chosen',
 332                  'style': 'width: 70%',
 333              }),
 334              'users_to_notify': forms.SelectMultiple(attrs={
 335                  'data-rel': 'chosen',
 336                  'style': 'width: 70%',
 337              }),
 338          }
 339  
 340      def __init__(self, *args, **kwargs):
 341          ret = super(InstanceForm, self).__init__(*args, **kwargs)
 342          self.fields['users_to_notify'].queryset = User.objects.filter(
 343              is_active=True).order_by('first_name', 'last_name')
 344          return ret
 345  
 346  
 347  class ServiceForm(forms.ModelForm):
 348      class Meta:
 349          model = Service
 350          template_name = 'service_form.html'
 351          fields = (
 352              'name',
 353              'url',
 354              'users_to_notify',
 355              'status_checks',
 356              'instances',
 357              'alerts',
 358              'alerts_enabled',
 359              'hackpad_id',
 360              'runbook_link',
 361              'is_public'
 362          )
 363          widgets = {
 364              'name': forms.TextInput(attrs={'style': 'width: 70%;'}),
 365              'url': forms.TextInput(attrs={'style': 'width: 70%;'}),
 366              'status_checks': forms.SelectMultiple(attrs={
 367                  'data-rel': 'chosen',
 368                  'style': 'width: 70%',
 369              }),
 370              'instances': forms.SelectMultiple(attrs={
 371                  'data-rel': 'chosen',
 372                  'style': 'width: 70%',
 373              }),
 374              'alerts': forms.SelectMultiple(attrs={
 375                  'data-rel': 'chosen',
 376                  'style': 'width: 70%',
 377              }),
 378              'users_to_notify': forms.SelectMultiple(attrs={
 379                  'data-rel': 'chosen',
 380                  'style': 'width: 70%',
 381              }),
 382              'hackpad_id': forms.TextInput(attrs={'style': 'width:70%;'}),
 383              'runbook_link': forms.TextInput(attrs={'style': 'width:70%;'}),
 384          }
 385  
 386      def __init__(self, *args, **kwargs):
 387          ret = super(ServiceForm, self).__init__(*args, **kwargs)
 388          self.fields['users_to_notify'].queryset = User.objects.filter(
 389              is_active=True).order_by('first_name', 'last_name')
 390          return ret
 391  
 392      def clean_hackpad_id(self):
 393          value = self.cleaned_data['hackpad_id']
 394          if not value:
 395              return ''
 396          for pattern in settings.RECOVERY_SNIPPETS_WHITELIST:
 397              if re.match(pattern, value):
 398                  return value
 399          raise ValidationError('Please specify a valid JS snippet link')
 400  
 401      def clean_runbook_link(self):
 402          value = self.cleaned_data['runbook_link']
 403          if not value:
 404              return ''
 405          try:
 406              URLValidator()(value)
 407              return value
 408          except ValidationError:
 409              raise ValidationError('Please specify a valid runbook link')
 410  
 411  class StatusCheckReportForm(forms.Form):
 412      service = forms.ModelChoiceField(
 413          queryset=Service.objects.all(),
 414          widget=forms.HiddenInput
 415      )
 416      checks = forms.ModelMultipleChoiceField(
 417          queryset=StatusCheck.objects.all(),
 418          widget=forms.SelectMultiple(
 419              attrs={
 420                  'data-rel': 'chosen',
 421                  'style': 'width: 70%',
 422              },
 423          )
 424      )
 425      date_from = forms.DateField(label='From', widget=forms.DateInput(attrs={'class': 'datepicker'}))
 426      date_to = forms.DateField(label='To', widget=forms.DateInput(attrs={'class': 'datepicker'}))
 427  
 428      def get_report(self):
 429          checks = self.cleaned_data['checks']
 430          now = timezone.now()
 431          for check in checks:
 432              # Group results of the check by status (failed alternating with succeeded),
 433              # take time of the first one in each group (starting from a failed group),
 434              # split them into pairs and form the list of problems.
 435              results = check.statuscheckresult_set.filter(
 436                  time__gte=self.cleaned_data['date_from'],
 437                  time__lt=self.cleaned_data['date_to'] + timedelta(days=1)
 438              ).order_by('time')
 439              groups = dropwhile(lambda item: item[0], groupby(results, key=lambda r: r.succeeded))
 440              times = [next(group).time for succeeded, group in groups]
 441              pairs = izip_longest(*([iter(times)] * 2))
 442              check.problems = [(start, end, (end or now) - start) for start, end in pairs]
 443              if results:
 444                  check.success_rate = results.filter(succeeded=True).count() / float(len(results)) * 100
 445          return checks
 446  
 447  
 448  class CheckCreateView(LoginRequiredMixin, CommonCreateView):
 449      template_name = 'cabotapp/statuscheck_form.html'
 450  
 451      def form_valid(self, form):
 452          form.instance.created_by = self.request.user
 453          return super(CheckCreateView, self).form_valid(form)
 454  
 455      def get_initial(self):
 456          if self.initial:
 457              initial = self.initial
 458          else:
 459              initial = {}
 460          metric = self.request.GET.get('metric')
 461          if metric:
 462              initial['metric'] = metric
 463          service_id = self.request.GET.get('service')
 464          instance_id = self.request.GET.get('instance')
 465  
 466          if service_id:
 467              try:
 468                  service = Service.objects.get(id=service_id)
 469                  initial['service_set'] = [service]
 470              except Service.DoesNotExist:
 471                  pass
 472  
 473          if instance_id:
 474              try:
 475                  instance = Instance.objects.get(id=instance_id)
 476                  initial['instance_set'] = [instance]
 477              except Instance.DoesNotExist:
 478                  pass
 479  
 480          return initial
 481  
 482      def get_success_url(self):
 483          if self.request.GET.get('service'):
 484              return reverse('service', kwargs={'pk': self.request.GET.get('service')})
 485          if self.request.GET.get('instance'):
 486              return reverse('instance', kwargs={'pk': self.request.GET.get('instance')})
 487          return reverse('checks')
 488  
 489  
 490  class CheckUpdateView(LoginRequiredMixin, CommonUpdateView):
 491      template_name = 'cabotapp/statuscheck_form.html'
 492  
 493      def get_success_url(self):
 494          return reverse('check', kwargs={'pk': self.object.id})
 495  
 496  
 497  class ICMPCheckCreateView(CheckCreateView):
 498      model = ICMPStatusCheck
 499      form_class = ICMPStatusCheckForm
 500  
 501  
 502  class ICMPCheckUpdateView(CheckUpdateView):
 503      model = ICMPStatusCheck
 504      form_class = ICMPStatusCheckForm
 505  
 506  
 507  class GraphiteCheckUpdateView(CheckUpdateView):
 508      model = GraphiteStatusCheck
 509      form_class = GraphiteStatusCheckForm
 510  
 511  
 512  class GraphiteCheckCreateView(CheckCreateView):
 513      model = GraphiteStatusCheck
 514      form_class = GraphiteStatusCheckForm
 515  
 516  
 517  class HttpCheckCreateView(CheckCreateView):
 518      model = HttpStatusCheck
 519      form_class = HttpStatusCheckForm
 520  
 521  
 522  class HttpCheckUpdateView(CheckUpdateView):
 523      model = HttpStatusCheck
 524      form_class = HttpStatusCheckForm
 525  
 526  
 527  class JenkinsCheckCreateView(CheckCreateView):
 528      model = JenkinsStatusCheck
 529      form_class = JenkinsStatusCheckForm
 530  
 531      def form_valid(self, form):
 532          form.instance.frequency = 1
 533          return super(JenkinsCheckCreateView, self).form_valid(form)
 534  
 535  
 536  class JenkinsCheckUpdateView(CheckUpdateView):
 537      model = JenkinsStatusCheck
 538      form_class = JenkinsStatusCheckForm
 539  
 540      def form_valid(self, form):
 541          form.instance.frequency = 1
 542          return super(JenkinsCheckUpdateView, self).form_valid(form)
 543  
 544  
 545  class StatusCheckListView(LoginRequiredMixin, CommonListView):
 546      model = StatusCheck
 547  
 548      def render_to_response(self, context, *args, **kwargs):
 549          context = super(StatusCheckListView, self).get_context_data(**kwargs)
 550          if context is None:
 551              context = {}
 552          context['checks'] = StatusCheck.objects.all().order_by('name').prefetch_related('service_set', 'instance_set')
 553          return super(StatusCheckListView, self).render_to_response(context, *args, **kwargs)
 554  
 555  
 556  class StatusCheckDeleteView(LoginRequiredMixin, CommonDeleteView):
 557      model = StatusCheck
 558      success_url = reverse_lazy('checks')
 559      context_object_name = 'check'
 560      template_name = 'cabotapp/statuscheck_confirm_delete.html'
 561  
 562  
 563  class StatusCheckDetailView(LoginRequiredMixin, CommonDetailView):
 564      model = StatusCheck
 565      context_object_name = 'check'
 566      template_name = 'cabotapp/statuscheck_detail.html'
 567  
 568      def render_to_response(self, context, *args, **kwargs):
 569          if context is None:
 570              context = {}
 571          checkresult_list = self.object.statuscheckresult_set.order_by(
 572              '-time_complete').all()
 573          paginator = Paginator(checkresult_list, 25)
 574  
 575          page = self.request.GET.get('page')
 576          try:
 577              checkresults = paginator.page(page)
 578          except PageNotAnInteger:
 579              checkresults = paginator.page(1)
 580          except EmptyPage:
 581              checkresults = paginator.page(paginator.num_pages)
 582  
 583          context['checkresults'] = checkresults
 584  
 585          return super(StatusCheckDetailView, self).render_to_response(context, *args, **kwargs)
 586  
 587  
 588  class UserProfileUpdateView(LoginRequiredMixin, View):
 589      model = AlertPluginUserData
 590  
 591      def get(self, *args, **kwargs):
 592          return HttpResponseRedirect(reverse('update-alert-user-data', args=(self.kwargs['pk'], u'General')))
 593  
 594  
 595  class UserProfileUpdateAlert(LoginRequiredMixin, View):
 596      template = loader.get_template('cabotapp/alertpluginuserdata_form.html')
 597      model = AlertPluginUserData
 598  
 599      def get(self, request, pk, alerttype):
 600          try:
 601              profile = UserProfile.objects.get(user=pk)
 602          except UserProfile.DoesNotExist:
 603              user = User.objects.get(id=pk)
 604              profile = UserProfile(user=user)
 605              profile.save()
 606  
 607          profile.user_data()
 608  
 609          if alerttype == u'General':
 610              form = GeneralSettingsForm(initial={
 611                  'first_name': profile.user.first_name,
 612                  'last_name': profile.user.last_name,
 613                  'email_address': profile.user.email,
 614                  'enabled': profile.user.is_active,
 615              })
 616          else:
 617              plugin_userdata = self.model.objects.get(title=alerttype, user=profile)
 618              form_model = get_object_form(type(plugin_userdata))
 619              form = form_model(instance=plugin_userdata)
 620  
 621          return render(request, self.template.template.name, {
 622              'form': form,
 623              'alert_preferences': profile.user_data(),
 624              'custom_check_types': get_custom_check_plugins(),
 625          })
 626  
 627      def post(self, request, pk, alerttype):
 628          profile = UserProfile.objects.get(user=pk)
 629          success = False
 630  
 631          if alerttype == u'General':
 632              form = GeneralSettingsForm(request.POST)
 633              if form.is_valid():
 634                  profile.user.first_name = form.cleaned_data['first_name']
 635                  profile.user.last_name = form.cleaned_data['last_name']
 636                  profile.user.is_active = form.cleaned_data['enabled']
 637                  profile.user.email = form.cleaned_data['email_address']
 638                  profile.user.save()
 639  
 640                  success = True
 641          else:
 642              plugin_userdata = self.model.objects.get(title=alerttype, user=profile)
 643              form_model = get_object_form(type(plugin_userdata))
 644              form = form_model(request.POST, instance=plugin_userdata)
 645              if form.is_valid():
 646                  form.save()
 647  
 648                  success = True
 649  
 650          if success:
 651              messages.add_message(request, messages.SUCCESS, 'Updated Successfully', extra_tags='success')
 652          else:
 653              messages.add_message(request, messages.ERROR, 'Error Updating Profile', extra_tags='danger')
 654  
 655          return HttpResponseRedirect(reverse('update-alert-user-data', args=(self.kwargs['pk'], alerttype)))
 656  
 657  
 658  class PluginSettingsView(LoginRequiredMixin, View):
 659      template = loader.get_template('cabotapp/plugin_settings_form.html')
 660      model = AlertPlugin
 661  
 662      def get(self, request, plugin_name):
 663          if plugin_name == u'global':
 664              form = CoreSettingsForm()
 665              alert_test_form = AlertTestForm()
 666          else:
 667              plugin = self.model.objects.get(title=plugin_name)
 668              form_model = get_object_form(type(plugin))
 669              form = form_model(instance=plugin)
 670              alert_test_form = AlertTestPluginForm(initial = {
 671                  'alert_plugin': plugin
 672              })
 673  
 674          return render(request, self.template.template.name, {
 675              'form': form,
 676              'plugins': AlertPlugin.objects.all(),
 677              'plugin_name': plugin_name,
 678              'alert_test_form': alert_test_form,
 679              'custom_check_types': get_custom_check_plugins()
 680          })
 681  
 682      def post(self, request, plugin_name):
 683          if plugin_name == u'global':
 684              form = CoreSettingsForm(request.POST)
 685          else:
 686              plugin = self.model.objects.get(title=plugin_name)
 687              form_model = get_object_form(type(plugin))
 688              form = form_model(request.POST, instance=plugin)
 689  
 690          if form.is_valid():
 691              form.save()
 692              messages.add_message(request, messages.SUCCESS, 'Updated Successfully', extra_tags='success')
 693          else:
 694              messages.add_message(request, messages.ERROR, 'Error Updating Plugin', extra_tags='danger')
 695  
 696          return HttpResponseRedirect(reverse('plugin-settings', args=(plugin_name,)))
 697  
 698  
 699  def get_object_form(model_type):
 700      class AlertPreferencesForm(forms.ModelForm):
 701          class Meta:
 702              model = model_type
 703              fields = '__all__'
 704  
 705          def is_valid(self):
 706              return True
 707  
 708      return AlertPreferencesForm
 709  
 710  
 711  class AlertTestForm(forms.Form):
 712      action = reverse_lazy('alert-test')
 713  
 714      service = forms.ModelChoiceField(
 715          queryset=Service.objects.all(),
 716          widget=forms.Select(attrs={
 717              'data-rel': 'chosen',
 718          })
 719      )
 720  
 721      STATUS_CHOICES = (
 722          (Service.PASSING_STATUS, 'Passing'),
 723          (Service.WARNING_STATUS, 'Warning'),
 724          (Service.ERROR_STATUS, 'Error'),
 725          (Service.CRITICAL_STATUS, 'Critical'),
 726      )
 727  
 728      old_status = forms.ChoiceField(
 729          choices=STATUS_CHOICES,
 730          initial=Service.PASSING_STATUS,
 731          widget=forms.Select(attrs={
 732              'data-rel': 'chosen',
 733          })
 734      )
 735  
 736      new_status = forms.ChoiceField(
 737          choices=STATUS_CHOICES,
 738          initial=Service.ERROR_STATUS,
 739          widget=forms.Select(attrs={
 740              'data-rel': 'chosen',
 741          })
 742      )
 743  
 744  
 745  class AlertTestPluginForm(AlertTestForm):
 746      action = reverse_lazy('alert-test-plugin')
 747  
 748      service = None
 749      alert_plugin = forms.ModelChoiceField(
 750          queryset=AlertPlugin.objects.filter(enabled=True),
 751          widget=forms.HiddenInput
 752      )
 753  
 754  
 755  class AlertTestView(LoginRequiredMixin, View):
 756      def trigger_alert_to_user(self, service, user, old_status, new_status):
 757          """
 758          Clear out all service users and duty shifts, and disable all fallback users.
 759          Then add a single shift for this user, and add this user to users-to-notify.
 760  
 761          This should ensure we never alert anyone except the user triggering the alert test.
 762          """
 763          with transaction.atomic():
 764              sid = transaction.savepoint()
 765              service.update_status()
 766              service.status_checks.update(active=False)
 767              service.overall_status = new_status
 768              service.old_overall_status = old_status
 769              service.last_alert_sent = None
 770  
 771              check = StatusCheck(name='ALERT_TEST')
 772              check.save()
 773              StatusCheckResult.objects.create(
 774                  status_check=check,
 775                  time=timezone.now(),
 776                  time_complete=timezone.now(),
 777                  succeeded=new_status == Service.PASSING_STATUS)
 778              check.last_run = timezone.now()
 779              check.save()
 780              service.status_checks.add(check)
 781              service.users_to_notify.clear()
 782              service.users_to_notify.add(user)
 783              service.unexpired_acknowledgements().delete()
 784              Shift.objects.update(deleted=True)
 785              UserProfile.objects.update(fallback_alert_user=False)
 786              Shift(
 787                  start=timezone.now() - timedelta(days=1),
 788                  end=timezone.now() + timedelta(days=1),
 789                  uid='test-shift',
 790                  last_modified=timezone.now(),
 791                  user=user
 792              ).save()
 793              service.alert()
 794              transaction.savepoint_rollback(sid)
 795  
 796      def post(self, request):
 797          form = AlertTestForm(request.POST)
 798  
 799          if form.is_valid():
 800              data = form.clean()
 801              service = data['service']
 802              self.trigger_alert_to_user(service, request.user, data['old_status'], data['new_status'])
 803  
 804              return JsonResponse({"result": "ok"})
 805          return JsonResponse({"result": "error"}, status=400)
 806  
 807  
 808  class AlertTestPluginView(AlertTestView):
 809      def post(self, request):
 810          form = AlertTestPluginForm(request.POST)
 811  
 812          if form.is_valid():
 813              data = form.clean()
 814  
 815              with transaction.atomic():
 816                  sid = transaction.savepoint()
 817  
 818                  service = Service.objects.create(
 819                      name='test-alert-service'
 820                  )
 821                  service.alerts.add(data['alert_plugin'])
 822                  self.trigger_alert_to_user(service, request.user, data['old_status'], data['new_status'])
 823  
 824                  transaction.savepoint_rollback(sid)
 825  
 826              return JsonResponse({"result": "ok"})
 827          return JsonResponse({"result": "error"}, status=400)
 828  
 829  
 830  class CoreSettingsForm(forms.Form):
 831      pass
 832  
 833  
 834  class GeneralSettingsForm(forms.Form):
 835      first_name = forms.CharField(label='First name', max_length=30, required=False)
 836      last_name = forms.CharField(label='Last name', max_length=30, required=False)
 837      email_address = forms.CharField(label='Email Address', max_length=75,
 838                                      required=False)  # We use 75 and not the 254 because Django 1.6.8 only supports
 839      # 75. See commit message for details.
 840      enabled = forms.BooleanField(label='Enabled', required=False)
 841  
 842  
 843  class InstanceListView(LoginRequiredMixin, CommonListView):
 844      model = Instance
 845      context_object_name = 'instances'
 846  
 847      def get_queryset(self):
 848          return Instance.objects.all().order_by('name').prefetch_related('status_checks')
 849  
 850  
 851  class ServiceListView(LoginRequiredMixin, CommonListView):
 852      model = Service
 853      context_object_name = 'services'
 854  
 855      def get_queryset(self):
 856          return Service.objects.all().order_by('name').prefetch_related('status_checks')
 857  
 858  
 859  class ServicePublicListView(TemplateView):
 860      model = Service
 861      context_object_name = 'services'
 862      template_name = "cabotapp/service_public_list.html"
 863  
 864      def get_context_data(self, **kwargs):
 865          context = super(ServicePublicListView, self).get_context_data(**kwargs)
 866          context[self.context_object_name] = Service.objects\
 867              .filter(is_public=True, alerts_enabled=True)\
 868              .order_by(Lower('name')).prefetch_related('status_checks')
 869          return context
 870  
 871  class InstanceDetailView(LoginRequiredMixin, CommonDetailView):
 872      model = Instance
 873      context_object_name = 'instance'
 874  
 875      def get_context_data(self, **kwargs):
 876          context = super(InstanceDetailView, self).get_context_data(**kwargs)
 877          date_from = date.today() - relativedelta(day=1)
 878          context['report_form'] = StatusCheckReportForm(initial={
 879              'checks': self.object.status_checks.all(),
 880              'service': self.object,
 881              'date_from': date_from,
 882              'date_to': date_from + relativedelta(months=1) - relativedelta(days=1)
 883          })
 884          return context
 885  
 886  
 887  class ServiceDetailView(LoginRequiredMixin, CommonDetailView):
 888      model = Service
 889      context_object_name = 'service'
 890  
 891      def get_context_data(self, **kwargs):
 892          context = super(ServiceDetailView, self).get_context_data(**kwargs)
 893          date_from = date.today() - relativedelta(day=1)
 894          context['report_form'] = StatusCheckReportForm(initial={
 895              'alerts': self.object.alerts.all(),
 896              'checks': self.object.status_checks.all(),
 897              'service': self.object,
 898              'date_from': date_from,
 899              'date_to': date_from + relativedelta(months=1) - relativedelta(days=1)
 900          })
 901          return context
 902  
 903  
 904  class InstanceCreateView(LoginRequiredMixin, CommonCreateView):
 905      model = Instance
 906      form_class = InstanceForm
 907  
 908      def form_valid(self, form):
 909          ret = super(InstanceCreateView, self).form_valid(form)
 910          if self.object.status_checks.filter(polymorphic_ctype__model='icmpstatuscheck').count() == 0:
 911              self.generate_default_ping_check(self.object)
 912          return ret
 913  
 914      def generate_default_ping_check(self, obj):
 915          pc = ICMPStatusCheck(
 916              name="Default Ping Check for %s" % obj.name,
 917              frequency=5,
 918              importance=Service.ERROR_STATUS,
 919              debounce=0,
 920              created_by=None,
 921          )
 922          pc.save()
 923          obj.status_checks.add(pc)
 924  
 925      def get_success_url(self):
 926          return reverse('instance', kwargs={'pk': self.object.id})
 927  
 928      def get_initial(self):
 929          if self.initial:
 930              initial = self.initial
 931          else:
 932              initial = {}
 933          service_id = self.request.GET.get('service')
 934  
 935          if service_id:
 936              try:
 937                  service = Service.objects.get(id=service_id)
 938                  initial['service_set'] = [service]
 939              except Service.DoesNotExist:
 940                  pass
 941  
 942          return initial
 943  
 944  
 945  @login_required
 946  def acknowledge_alert(request, pk):
 947      service = Service.objects.get(pk=pk)
 948      service.acknowledge_alert(user=request.user)
 949      return HttpResponseRedirect(reverse('service', kwargs={'pk': pk}))
 950  
 951  
 952  @login_required
 953  def remove_acknowledgement(request, pk):
 954      service = Service.objects.get(pk=pk)
 955      service.remove_acknowledgement(user=request.user)
 956      return HttpResponseRedirect(reverse('service', kwargs={'pk': pk}))
 957  
 958  
 959  class ServiceCreateView(LoginRequiredMixin, CommonCreateView):
 960      model = Service
 961      form_class = ServiceForm
 962  
 963      def __init__(self, *args, **kwargs):
 964          super(ServiceCreateView, self).__init__(*args, **kwargs)
 965  
 966      def get_success_url(self):
 967          return reverse('service', kwargs={'pk': self.object.id})
 968  
 969  
 970  class InstanceUpdateView(LoginRequiredMixin, CommonUpdateView):
 971      model = Instance
 972      form_class = InstanceForm
 973  
 974      def get_success_url(self):
 975          return reverse('instance', kwargs={'pk': self.object.id})
 976  
 977  
 978  class ServiceUpdateView(LoginRequiredMixin, CommonUpdateView):
 979      model = Service
 980      form_class = ServiceForm
 981  
 982      def get_success_url(self):
 983          return reverse('service', kwargs={'pk': self.object.id})
 984  
 985  
 986  class ServiceDeleteView(LoginRequiredMixin, CommonDeleteView):
 987      model = Service
 988      success_url = reverse_lazy('services')
 989      context_object_name = 'service'
 990      template_name = 'cabotapp/service_confirm_delete.html'
 991  
 992  
 993  class InstanceDeleteView(LoginRequiredMixin, CommonDeleteView):
 994      model = Instance
 995      success_url = reverse_lazy('instances')
 996      context_object_name = 'instance'
 997      template_name = 'cabotapp/instance_confirm_delete.html'
 998  
 999  
1000  class ShiftListView(LoginRequiredMixin, CommonListView):
1001      model = Shift
1002      context_object_name = 'shifts'
1003  
1004      def get_queryset(self):
1005          return Shift.objects.filter(
1006              end__gt=datetime.utcnow().replace(tzinfo=utc),
1007              deleted=False).order_by('start')
1008  
1009  
1010  class StatusCheckReportView(LoginRequiredMixin, TemplateView):
1011      template_name = 'cabotapp/statuscheck_report.html'
1012  
1013      def get_context_data(self, **kwargs):
1014          form = StatusCheckReportForm(self.request.GET)
1015          if form.is_valid():
1016              return {'checks': form.get_report(), 'service': form.cleaned_data['service']}
1017  
1018  
1019  class SetupForm(forms.Form):
1020      username = forms.CharField(label='Username', max_length=100, required=True)
1021      email = forms.EmailField(label='Email', max_length=200, required=False)
1022      password = forms.CharField(label='Password', required=True, widget=forms.PasswordInput())
1023  
1024  
1025  class SetupView(View):
1026      template = loader.get_template('cabotapp/setup.html')
1027  
1028      def get(self, request):
1029          if not cabot_needs_setup():
1030              return redirect('login')
1031  
1032          form = SetupForm(initial={
1033              'username': 'admin',
1034          })
1035  
1036          return HttpResponse(self.template.render({'form': form}, request))
1037  
1038      def post(self, request):
1039          if not cabot_needs_setup():
1040              return redirect('login')
1041  
1042          form = SetupForm(request.POST)
1043          if form.is_valid():
1044              get_user_model().objects.create_superuser(
1045                  username=form.cleaned_data['username'],
1046                  email=form.cleaned_data['email'],
1047                  password=form.cleaned_data['password'],
1048              )
1049              return redirect('login')
1050  
1051          return HttpResponse(self.template.render({'form': form}, request), status=400)
1052  
1053  
1054  class OnCallView(APIView):
1055      queryset = User.objects
1056  
1057      def get(self, request):
1058          users = get_duty_officers()
1059  
1060          users_json = []
1061          for user in users:
1062              plugin_data = {}
1063              for pluginuserdata in user.profile.alertpluginuserdata_set.all():
1064                  plugin_data[pluginuserdata.title] = pluginuserdata.serialize()
1065  
1066              users_json.append({
1067                      "username": user.username,
1068                      "email": user.email,
1069                      "mobile_number": user.profile.mobile_number,
1070                      "plugin_data": plugin_data
1071                  })
1072  
1073          return Response(users_json)
1074  
1075  
1076  # Misc JSON api and other stuff
1077  
1078  
1079  def checks_run_recently(request):
1080      """
1081      Checks whether or not stuff is running by looking to see if checks have run in last 10 mins
1082      """
1083      ten_mins = datetime.utcnow().replace(tzinfo=utc) - timedelta(minutes=10)
1084      most_recent = StatusCheckResult.objects.filter(time_complete__gte=ten_mins)
1085      if most_recent.exists():
1086          return HttpResponse('Checks running')
1087      return HttpResponse('Checks not running')
1088  
1089  
1090  def about(request):
1091      """ Very simple about page """
1092      from cabot import version
1093  
1094      return render(request, 'cabotapp/about.html', {
1095          'cabot_version': version,
1096      })
1097  
1098  def jsonify(d):
1099      return HttpResponse(json.dumps(d), content_type='application/json')
1100  
1101  
1102  @login_required
1103  def graphite_api_data(request):
1104      metric = request.GET.get('metric')
1105      if request.GET.get('frequency'):
1106          mins_to_check = int(request.GET.get('frequency'))
1107      else:
1108          mins_to_check = None
1109      data = None
1110      matching_metrics = None
1111      try:
1112          data = get_data(metric, mins_to_check)
1113      except requests.exceptions.RequestException, e:
1114          pass
1115      if not data:
1116          try:
1117              matching_metrics = get_matching_metrics(metric)
1118          except requests.exceptions.RequestException, e:
1119              return jsonify({'status': 'error', 'message': str(e)})
1120          matching_metrics = {'metrics': matching_metrics}
1121      return jsonify({'status': 'ok', 'data': data, 'matchingMetrics': matching_metrics})