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})