diff --git a/app/cabotapp/admin.py b/app/cabotapp/admin.py index 8ab4972..2da3a86 100644 --- a/app/cabotapp/admin.py +++ b/app/cabotapp/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import UserProfile, Service, Shift, ServiceStatusSnapshot, StatusCheck, StatusCheckResult +from .models import UserProfile, Service, Shift, ServiceStatusSnapshot, StatusCheck, StatusCheckResult, Instance admin.site.register(UserProfile) admin.site.register(Shift) @@ -7,3 +7,4 @@ admin.site.register(Service) admin.site.register(ServiceStatusSnapshot) admin.site.register(StatusCheck) admin.site.register(StatusCheckResult) +admin.site.register(Instance) diff --git a/app/cabotapp/migrations/0004_auto__add_instance__del_field_service_telephone_alert__del_field_servi.py b/app/cabotapp/migrations/0004_auto__add_instance__del_field_service_telephone_alert__del_field_servi.py new file mode 100644 index 0000000..d5183bb --- /dev/null +++ b/app/cabotapp/migrations/0004_auto__add_instance__del_field_service_telephone_alert__del_field_servi.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Instance' + db.create_table('cabotapp_instance', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('last_alert_sent', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + ('email_alert', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('hipchat_alert', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('sms_alert', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('telephone_alert', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('alerts_enabled', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('overall_status', self.gf('django.db.models.fields.TextField')(default='PASSING')), + ('old_overall_status', self.gf('django.db.models.fields.TextField')(default='PASSING')), + ('hackpad_id', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('name', self.gf('django.db.models.fields.TextField')()), + ('address', self.gf('django.db.models.fields.TextField')(blank=True)), + )) + db.send_create_signal('cabotapp', ['Instance']) + + # Adding M2M table for field users_to_notify on 'Instance' + db.create_table('cabotapp_instance_users_to_notify', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('instance', models.ForeignKey(orm['cabotapp.instance'], null=False)), + ('user', models.ForeignKey(orm['auth.user'], null=False)) + )) + db.create_unique('cabotapp_instance_users_to_notify', ['instance_id', 'user_id']) + + # Adding M2M table for field status_checks on 'Instance' + db.create_table('cabotapp_instance_status_checks', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('instance', models.ForeignKey(orm['cabotapp.instance'], null=False)), + ('statuscheck', models.ForeignKey(orm['cabotapp.statuscheck'], null=False)) + )) + db.create_unique('cabotapp_instance_status_checks', ['instance_id', 'statuscheck_id']) + + # Adding M2M table for field services on 'Instance' + db.create_table('cabotapp_instance_services', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('instance', models.ForeignKey(orm['cabotapp.instance'], null=False)), + ('service', models.ForeignKey(orm['cabotapp.service'], null=False)) + )) + db.create_unique('cabotapp_instance_services', ['instance_id', 'service_id']) + + # Adding M2M table for field instances on 'Service' + db.create_table('cabotapp_service_instances', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('service', models.ForeignKey(orm['cabotapp.service'], null=False)), + ('instance', models.ForeignKey(orm['cabotapp.instance'], null=False)) + )) + db.create_unique('cabotapp_service_instances', ['service_id', 'instance_id']) + + + def backwards(self, orm): + # Deleting model 'Instance' + db.delete_table('cabotapp_instance') + + # Removing M2M table for field users_to_notify on 'Instance' + db.delete_table('cabotapp_instance_users_to_notify') + + # Removing M2M table for field status_checks on 'Instance' + db.delete_table('cabotapp_instance_status_checks') + + # Removing M2M table for field services on 'Instance' + db.delete_table('cabotapp_instance_services') + + # Removing M2M table for field instances on 'Service' + db.delete_table('cabotapp_service_instances') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'cabotapp.instance': { + 'Meta': {'ordering': "['name']", 'object_name': 'Instance'}, + 'address': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'alerts_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'email_alert': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hackpad_id': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'hipchat_alert': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_alert_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.TextField', [], {}), + 'old_overall_status': ('django.db.models.fields.TextField', [], {'default': "'PASSING'"}), + 'overall_status': ('django.db.models.fields.TextField', [], {'default': "'PASSING'"}), + 'services': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['cabotapp.Service']", 'symmetrical': 'False', 'blank': 'True'}), + 'sms_alert': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'status_checks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['cabotapp.StatusCheck']", 'symmetrical': 'False', 'blank': 'True'}), + 'telephone_alert': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'users_to_notify': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'cabotapp.service': { + 'Meta': {'ordering': "['name']", 'object_name': 'Service'}, + 'alerts_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'email_alert': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hackpad_id': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'hipchat_alert': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instances': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['cabotapp.Instance']", 'symmetrical': 'False', 'blank': 'True'}), + 'last_alert_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.TextField', [], {}), + 'old_overall_status': ('django.db.models.fields.TextField', [], {'default': "'PASSING'"}), + 'overall_status': ('django.db.models.fields.TextField', [], {'default': "'PASSING'"}), + 'sms_alert': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'status_checks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['cabotapp.StatusCheck']", 'symmetrical': 'False', 'blank': 'True'}), + 'telephone_alert': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'url': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'users_to_notify': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'cabotapp.servicestatussnapshot': { + 'Meta': {'object_name': 'ServiceStatusSnapshot'}, + 'did_send_alert': ('django.db.models.fields.IntegerField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'num_checks_active': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'num_checks_failing': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'num_checks_passing': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'overall_status': ('django.db.models.fields.TextField', [], {'default': "'PASSING'"}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'snapshots'", 'to': "orm['cabotapp.Service']"}), + 'time': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}) + }, + 'cabotapp.shift': { + 'Meta': {'object_name': 'Shift'}, + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'end': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'start': ('django.db.models.fields.DateTimeField', [], {}), + 'uid': ('django.db.models.fields.TextField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'cabotapp.statuscheck': { + 'Meta': {'ordering': "['name']", 'object_name': 'StatusCheck'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cached_health': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'calculated_status': ('django.db.models.fields.CharField', [], {'default': "'passing'", 'max_length': '50', 'blank': 'True'}), + 'check_type': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'debounce': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True'}), + 'endpoint': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'expected_num_hosts': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True'}), + 'frequency': ('django.db.models.fields.IntegerField', [], {'default': '5'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'importance': ('django.db.models.fields.CharField', [], {'default': "'ERROR'", 'max_length': '30'}), + 'last_run': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'max_queued_build_time': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'metric': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'name': ('django.db.models.fields.TextField', [], {}), + 'password': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'polymorphic_ctype': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'polymorphic_cabotapp.statuscheck_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'status_code': ('django.db.models.fields.TextField', [], {'default': '200', 'null': 'True'}), + 'text_match': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'timeout': ('django.db.models.fields.IntegerField', [], {'default': '30', 'null': 'True'}), + 'username': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'value': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'verify_ssl_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'cabotapp.statuscheckresult': { + 'Meta': {'object_name': 'StatusCheckResult'}, + 'check': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cabotapp.StatusCheck']"}), + 'error': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'raw_data': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'succeeded': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'time': ('django.db.models.fields.DateTimeField', [], {}), + 'time_complete': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}) + }, + 'cabotapp.userprofile': { + 'Meta': {'object_name': 'UserProfile'}, + 'fallback_alert_user': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hipchat_alias': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mobile_number': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['cabotapp'] \ No newline at end of file diff --git a/app/cabotapp/models.py b/app/cabotapp/models.py index e21198b..c245896 100644 --- a/app/cabotapp/models.py +++ b/app/cabotapp/models.py @@ -17,6 +17,7 @@ from django.utils import timezone import json import re import time +import os import requests from celery.utils.log import get_task_logger @@ -65,7 +66,11 @@ def calculate_debounced_passing(recent_results, debounce=0): return False -class Service(models.Model): +class CheckGroupMixin(models.Model): + + class Meta: + abstract = True + PASSING_STATUS = 'PASSING' WARNING_STATUS = 'WARNING' ERROR_STATUS = 'ERROR' @@ -88,10 +93,7 @@ class Service(models.Model): ) name = models.TextField() - url = models.TextField( - blank=True, - help_text="URL of service." - ) + users_to_notify = models.ManyToManyField( User, blank=True, @@ -126,8 +128,6 @@ class Service(models.Model): help_text='Gist, Hackpad or Refheap js embed with recovery instructions e.g. https://you.hackpad.com/some_document.js' ) - class Meta: - ordering = ['name'] def __unicode__(self): return self.name @@ -201,18 +201,6 @@ class Service(models.Model): s['time'] = time.mktime(s['time'].timetuple()) return snapshots - def active_status_checks(self): - return self.status_checks.filter(active=True) - - def inactive_status_checks(self): - return self.status_checks.filter(active=False) - - def all_passing_checks(self): - return self.active_status_checks().filter(calculated_status=self.CALCULATED_PASSING_STATUS) - - def all_failing_checks(self): - return self.active_status_checks().exclude(calculated_status=self.CALCULATED_PASSING_STATUS) - def graphite_status_checks(self): return self.status_checks.filter(polymorphic_ctype__model='graphitestatuscheck') @@ -231,6 +219,53 @@ class Service(models.Model): def active_jenkins_status_checks(self): return self.jenkins_status_checks().filter(active=True) + def active_status_checks(self): + return self.status_checks.filter(active=True) + + def inactive_status_checks(self): + return self.status_checks.filter(active=False) + + def all_passing_checks(self): + return self.active_status_checks().filter(calculated_status=self.CALCULATED_PASSING_STATUS) + + def all_failing_checks(self): + return self.active_status_checks().exclude(calculated_status=self.CALCULATED_PASSING_STATUS) + +class Service(CheckGroupMixin): + + instances = models.ManyToManyField( + 'Instance', + blank=True, + help_text='Instances this service is running on.', + ) + + + url = models.TextField( + blank=True, + help_text="URL of service." + ) + + class Meta: + ordering = ['name'] + + +class Instance(CheckGroupMixin): + + + class Meta: + ordering = ['name'] + + address = models.TextField( + blank=True, + help_text="Address (IP/Hostname) of service." + ) + + def icmp_status_checks(self): + return self.status_checks.filter(polymorphic_ctype__model='icmpstatuscheck') + + def active_icmp_status_checks(self): + return self.icmp_status_checks().filter(active=True) + class ServiceStatusSnapshot(models.Model): service = models.ForeignKey(Service, related_name='snapshots') @@ -399,6 +434,28 @@ class StatusCheck(PolymorphicModel): for service in services: update_service.delay(service.id) +class ICMPStatusCheck(StatusCheck): + + + class Meta(StatusCheck.Meta): + proxy = True + + @property + def check_category(self): + return "ICMP/Ping Check" + + def _run(self): + result = StatusCheckResult(check=self) + instances = self.instance_set.all() + target = self.instance_set.get().address + response = os.system("ping -c 1 " + target) + if response == 0: + result.succeeded = True + else: + result.succeeded = False + result.error = "Could not connect, host is most likely down" + return result + class GraphiteStatusCheck(StatusCheck): @@ -532,7 +589,6 @@ class HttpStatusCheck(StatusCheck): result.succeeded = True return result - class JenkinsStatusCheck(StatusCheck): class Meta(StatusCheck.Meta): diff --git a/app/cabotapp/views.py b/app/cabotapp/views.py index 4067245..e002233 100644 --- a/app/cabotapp/views.py +++ b/app/cabotapp/views.py @@ -5,8 +5,8 @@ from django.http import HttpResponse, HttpResponseRedirect from django.core.urlresolvers import reverse_lazy from django.conf import settings from models import ( - StatusCheck, GraphiteStatusCheck, JenkinsStatusCheck, HttpStatusCheck, - StatusCheckResult, UserProfile, Service, Shift, get_duty_officers) + StatusCheck, GraphiteStatusCheck, JenkinsStatusCheck, HttpStatusCheck, ICMPStatusCheck, + StatusCheckResult, UserProfile, Service, Instance, Shift, get_duty_officers) from tasks import run_status_check as _run_status_check from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator @@ -59,7 +59,6 @@ class StatusCheckResultDetailView(LoginRequiredMixin, DetailView): model = StatusCheckResult context_object_name = 'result' - class SymmetricalForm(forms.ModelForm): symmetrical_fields = () # Iterable of 2-tuples (field, model) @@ -90,7 +89,7 @@ base_widgets = { class StatusCheckForm(SymmetricalForm): - symmetrical_fields = ('service_set',) + symmetrical_fields = ('service_set', 'instance_set') service_set = forms.ModelMultipleChoiceField( queryset=Service.objects.all(), required=False, @@ -103,6 +102,17 @@ class StatusCheckForm(SymmetricalForm): ) ) + instance_set = forms.ModelMultipleChoiceField( + queryset=Instance.objects.all(), + required=False, + help_text='Link to instance(s).', + widget=forms.SelectMultiple( + attrs={ + 'data-rel': 'chosen', + 'style': 'width: 70%', + }, + ) + ) class GraphiteStatusCheckForm(StatusCheckForm): @@ -135,6 +145,19 @@ class GraphiteStatusCheckForm(StatusCheckForm): }) +class ICMPStatusCheckForm(StatusCheckForm): + + class Meta: + model = ICMPStatusCheck + fields = ( + 'name', + 'frequency', + 'importance', + 'active', + 'debounce', + ) + widgets = dict(**base_widgets) + class HttpStatusCheckForm(StatusCheckForm): class Meta: @@ -195,6 +218,57 @@ class UserProfileForm(forms.ModelForm): model = UserProfile exclude = ('user',) +class InstanceForm(SymmetricalForm): + + symmetrical_fields = ('service_set',) + service_set = forms.ModelMultipleChoiceField( + queryset=Service.objects.all(), + required=False, + help_text='Link to service(s).', + widget=forms.SelectMultiple( + attrs={ + 'data-rel': 'chosen', + 'style': 'width: 70%', + }, + ) + ) + + + class Meta: + model = Instance + template_name = 'instance_form.html' + fields = ( + 'name', + 'address', + 'users_to_notify', + 'status_checks', + 'service_set', + 'email_alert', + 'hipchat_alert', + 'sms_alert', + 'telephone_alert', + 'alerts_enabled', + ) + widgets = { + 'name': forms.TextInput(attrs={'style': 'width: 30%;'}), + 'address': forms.TextInput(attrs={'style': 'width: 70%;'}), + 'status_checks': forms.SelectMultiple(attrs={ + 'data-rel': 'chosen', + 'style': 'width: 70%', + }), + 'service_set': forms.SelectMultiple(attrs={ + 'data-rel': 'chosen', + 'style': 'width: 70%', + }), + 'users_to_notify': forms.CheckboxSelectMultiple(), + } + + def __init__(self, *args, **kwargs): + ret = super(InstanceForm, self).__init__(*args, **kwargs) + self.fields['users_to_notify'].queryset = User.objects.filter( + is_active=True) + return ret + class ServiceForm(forms.ModelForm): @@ -206,6 +280,7 @@ class ServiceForm(forms.ModelForm): 'url', 'users_to_notify', 'status_checks', + 'instances', 'email_alert', 'hipchat_alert', 'sms_alert', @@ -220,6 +295,10 @@ class ServiceForm(forms.ModelForm): 'data-rel': 'chosen', 'style': 'width: 70%', }), + 'instances': forms.SelectMultiple(attrs={ + 'data-rel': 'chosen', + 'style': 'width: 70%', + }), 'users_to_notify': forms.CheckboxSelectMultiple(), 'hackpad_id': forms.TextInput(attrs={'style': 'width:30%;'}), } @@ -293,20 +372,30 @@ class CheckCreateView(LoginRequiredMixin, CreateView): if metric: initial['metric'] = metric service_id = self.request.GET.get('service') + instance_id = self.request.GET.get('instance') + if service_id: try: service = Service.objects.get(id=service_id) initial['service_set'] = [service] except Service.DoesNotExist: pass + + if instance_id: + try: + instance = Instance.objects.get(id=instance_id) + initial['instance_set'] = [instance] + except Instance.DoesNotExist: + pass + return initial def get_success_url(self): - if self.request.GET.get('service') != '': + if self.request.GET.get('service'): return reverse('service', kwargs={'pk': self.request.GET.get('service')}) - else: - return reverse('checks') - + if self.request.GET.get('instance'): + return reverse('instance', kwargs={'pk': self.request.GET.get('instance')}) + return reverse('checks') class CheckUpdateView(LoginRequiredMixin, UpdateView): @@ -315,8 +404,16 @@ class CheckUpdateView(LoginRequiredMixin, UpdateView): def get_success_url(self): return reverse('check', kwargs={'pk': self.object.id}) +class ICMPCheckCreateView(CheckCreateView): + model = ICMPStatusCheck + form_class = ICMPStatusCheckForm -class GraphiteCheckCreateView(CheckCreateView): + +class ICMPCheckUpdateView(CheckUpdateView): + model = ICMPStatusCheck + form_class = ICMPStatusCheckForm + +class ICMPhiteCheckCreateView(CheckCreateView): model = GraphiteStatusCheck form_class = GraphiteStatusCheckForm @@ -325,6 +422,9 @@ class GraphiteCheckUpdateView(CheckUpdateView): model = GraphiteStatusCheck form_class = GraphiteStatusCheckForm +class GraphiteCheckCreateView(CheckCreateView): + model = GraphiteStatusCheck + form_class = GraphiteStatusCheckForm class HttpCheckCreateView(CheckCreateView): model = HttpStatusCheck @@ -397,6 +497,14 @@ class UserProfileUpdateView(LoginRequiredMixin, UpdateView): return profile +class InstanceListView(LoginRequiredMixin, ListView): + + model = Instance + context_object_name = 'instances' + + def get_queryset(self): + return Instance.objects.all().order_by('name').prefetch_related('status_checks') + class ServiceListView(LoginRequiredMixin, ListView): model = Service context_object_name = 'services' @@ -404,6 +512,20 @@ class ServiceListView(LoginRequiredMixin, ListView): def get_queryset(self): return Service.objects.all().order_by('name').prefetch_related('status_checks') +class InstanceDetailView(LoginRequiredMixin, DetailView): + model = Instance + context_object_name = 'instance' + + def get_context_data(self, **kwargs): + context = super(InstanceDetailView, self).get_context_data(**kwargs) + date_from = date.today() - relativedelta(day=1) + context['report_form'] = StatusCheckReportForm(initial={ + 'checks': self.object.status_checks.all(), + 'service': self.object, + 'date_from': date_from, + 'date_to': date_from + relativedelta(months=1) - relativedelta(days=1) + }) + return context class ServiceDetailView(LoginRequiredMixin, DetailView): model = Service @@ -421,6 +543,43 @@ class ServiceDetailView(LoginRequiredMixin, DetailView): return context +class InstanceCreateView(LoginRequiredMixin, CreateView): + model = Instance + form_class = InstanceForm + + def generateDefaultPingCheck(self): + pc = ICMPStatusCheck() + pc.created_by = self.request.user + pc.name = "Default Ping Check" + pc.frequency = 5 + pc.importance = Service.ERROR_STATUS + pc.active = True + pc.debounce = 0 + pc.save() + pc.instance_set = [Instance.objects.get(pk=self.object.id)] + pc.save() + + def get_success_url(self): +#Where else can I run things when an instance gets created? + self.generateDefaultPingCheck() + return reverse('instance', kwargs={'pk': self.object.id}) + + def get_initial(self): + if self.initial: + initial = self.initial + else: + initial = {} + service_id = self.request.GET.get('service') + + if service_id: + try: + service = Service.objects.get(id=service_id) + initial['service_set'] = [service] + except Service.DoesNotExist: + pass + + return initial + class ServiceCreateView(LoginRequiredMixin, CreateView): model = Service form_class = ServiceForm @@ -428,6 +587,12 @@ class ServiceCreateView(LoginRequiredMixin, CreateView): def get_success_url(self): return reverse('service', kwargs={'pk': self.object.id}) +class InstanceUpdateView(LoginRequiredMixin, UpdateView): + model = Instance + form_class = InstanceForm + + def get_success_url(self): + return reverse('instance', kwargs={'pk': self.object.id}) class ServiceUpdateView(LoginRequiredMixin, UpdateView): model = Service @@ -443,6 +608,11 @@ class ServiceDeleteView(LoginRequiredMixin, DeleteView): context_object_name = 'service' template_name = 'cabotapp/service_confirm_delete.html' +class InstanceDeleteView(LoginRequiredMixin, DeleteView): + model = Instance + success_url = reverse_lazy('instances') + context_object_name = 'instance' + template_name = 'cabotapp/instance_confirm_delete.html' class ShiftListView(LoginRequiredMixin, ListView): model = Shift diff --git a/app/templates/base.html b/app/templates/base.html index 1f1016b..a862060 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -41,6 +41,9 @@ @@ -37,6 +40,7 @@ Test description Importance Service(s) + Instance(s) @@ -81,6 +85,18 @@ No service {% endif %} + + {% for instance in check.instance_set.all %} + {{ instance.name }} + {% if forloop.last %} + {% else %} + / + {% endif %} + {% endfor %} + {% if not check.instance_set.all %} + No instance + {% endif %} + @@ -100,4 +116,4 @@ {% endif %} - \ No newline at end of file + diff --git a/app/templates/cabotapp/_statuscheck_list_instance.html b/app/templates/cabotapp/_statuscheck_list_instance.html new file mode 100644 index 0000000..d851a70 --- /dev/null +++ b/app/templates/cabotapp/_statuscheck_list_instance.html @@ -0,0 +1,119 @@ +{% load extra %} + +
+
+

+

{{ checks_type }} checks

+
+

+  {% if checks_type == "All" or checks_type == "Graphite" %} + + {% endif %} + {% if checks_type == "All" or checks_type == "Http" %} +   + {% endif %} + {% if checks_type == "All" or checks_type == "Jenkins" %} +   + {% endif %} + {% if checks_type == "All" or checks_type == "ICMP" %} +   + {% endif %} +

+
+
+
+
+
+
+ {% if not checks %} +
No checks configured
+ {% else %} + + + + + + + {% if checks_type == "All" %} + + {% endif %} + + + + + + + + + {% for check in checks %} + + + + + {% if checks_type == "All" %} + + {% endif %} + + + + + + + {% endfor %} + +
NameStatusTypeTest descriptionImportanceService(s)Instance(s)
+ {{check.name}} + + {% if check.active %} + + {{ check.calculated_status|capfirst }} + + {% else %} + Disabled + {% endif %} + + {% if not check.recent_results %} + No results available + {% endif %} + + + + {% if check.polymorphic_ctype.model == 'graphitestatuscheck' %}{{ check.metric|truncatechars:70 }} {{ check.check_type }} {{ check.value }}{% if check.expected_num_hosts %} (from {{ check.expected_num_hosts }} hosts){% endif %}{% elif check.polymorphic_ctype.model == 'httpstatuscheck' %}Status code {{ check.status_code }} from {{ check.endpoint }}{% if check.text_match %}; match text /{{ check.text_match }}/{% endif %}{% elif check.polymorphic_ctype.model == 'jenkinsstatuscheck' %}Monitor job {{ check.name }}{% if check.max_queued_build_time %}; check no build waiting for >{{ check.max_queued_build_time }} minutes{% endif %}{% endif %} + {{ check.get_importance_display }} + {% for service in check.service_set.all %} + {{ service.name }} + {% if forloop.last %} + {% else %} + / + {% endif %} + {% endfor %} + {% if not check.service_set.all %} + No service + {% endif %} + + {% for instance in check.instance_set.all %} + {{ instance.name }} + {% if forloop.last %} + {% else %} + / + {% endif %} + {% endfor %} + {% if not check.instance_set.all %} + No instance + {% endif %} + + + + + + + + {% if checks_type == "Jenkins" %} + + + + {% endif %} +
+ {% endif %} +
+
diff --git a/app/templates/cabotapp/_statuscheck_list_service.html b/app/templates/cabotapp/_statuscheck_list_service.html new file mode 100644 index 0000000..6f3b759 --- /dev/null +++ b/app/templates/cabotapp/_statuscheck_list_service.html @@ -0,0 +1,103 @@ +{% load extra %} + +
+
+

+

{{ checks_type }} checks

+
+

+  {% if checks_type == "All" or checks_type == "Graphite" %} + + {% endif %} + {% if checks_type == "All" or checks_type == "Http" %} +   + {% endif %} + {% if checks_type == "All" or checks_type == "Jenkins" %} +   + {% endif %} +

+
+
+
+
+
+
+ {% if not checks %} +
No checks configured
+ {% else %} + + + + + + + {% if checks_type == "All" %} + + {% endif %} + + + + + + + + {% for check in checks %} + + + + + {% if checks_type == "All" %} + + {% endif %} + + + + + + {% endfor %} + +
NameStatusTypeTest descriptionImportanceService(s)
+ {{check.name}} + + {% if check.active %} + + {{ check.calculated_status|capfirst }} + + {% else %} + Disabled + {% endif %} + + {% if not check.recent_results %} + No results available + {% endif %} + + + + {% if check.polymorphic_ctype.model == 'graphitestatuscheck' %}{{ check.metric|truncatechars:70 }} {{ check.check_type }} {{ check.value }}{% if check.expected_num_hosts %} (from {{ check.expected_num_hosts }} hosts){% endif %}{% elif check.polymorphic_ctype.model == 'httpstatuscheck' %}Status code {{ check.status_code }} from {{ check.endpoint }}{% if check.text_match %}; match text /{{ check.text_match }}/{% endif %}{% elif check.polymorphic_ctype.model == 'jenkinsstatuscheck' %}Monitor job {{ check.name }}{% if check.max_queued_build_time %}; check no build waiting for >{{ check.max_queued_build_time }} minutes{% endif %}{% endif %} + {{ check.get_importance_display }} + {% for service in check.service_set.all %} + {{ service.name }} + {% if forloop.last %} + {% else %} + / + {% endif %} + {% endfor %} + {% if not check.service_set.all %} + No service + {% endif %} + + + + + + + + {% if checks_type == "Jenkins" %} + + + + {% endif %} +
+ {% endif %} +
+
diff --git a/app/templates/cabotapp/instance_confirm_delete.html b/app/templates/cabotapp/instance_confirm_delete.html new file mode 100644 index 0000000..dd037ed --- /dev/null +++ b/app/templates/cabotapp/instance_confirm_delete.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block content %} +

Delete service

+
{% csrf_token %} + +
+{% endblock %} + +{% load compress %} +{% block js %} +{{ block.super }} +{% compress js %} + +{% endcompress %} +{% endblock %} diff --git a/app/templates/cabotapp/instance_detail.html b/app/templates/cabotapp/instance_detail.html new file mode 100644 index 0000000..d16faa4 --- /dev/null +++ b/app/templates/cabotapp/instance_detail.html @@ -0,0 +1,225 @@ +{% extends 'base.html' %} + +{% block title %}{{ block.super }} - {{ instance.name }}{% endblock title %} + +{% block content %} + +
+
+

+

{{ instance.name }}

+

{{ instance.overall_status|lower|capfirst }} {% if instance.alerts_enabled %}Alerts enabled{%else %}Alerts disabled{% endif %}

+

+
+
+
+
+
+

+

Configuration

+
+
+
Address
+
{{ instance.address|urlize|default:"None configured" }}
+
+
+
+
Users watching
+
+ {% if not instance.users_to_notify.all %} + No users subscribed + {% else %} + {{ instance.users_to_notify.all|join:", " }} + {% endif %} +
+
+
+
+
Alert types
+
+
+ {% if instance.email_alert %} Email{% endif %} + {% if instance.hipchat_alert %} Hipchat{% endif %} + {% if instance.sms_alert %} SMS{% endif %} + {% if instance.telephone_alert %} Telephone{% endif %} +
+
+
+
+
+

+

Status (24 hours)

+
+
+
+
+
+
+ +
+
+

+

Services

+
+{% include 'cabotapp/_service_list.html' with services=instance.service_set.all %} +
+ + +
+ +{% include 'cabotapp/_statuscheck_list_instance.html' with checks=instance.graphite_status_checks.all instance=instance checks_type="Graphite" %} + +
+ +{% include 'cabotapp/_statuscheck_list_instance.html' with checks=instance.http_status_checks.all instance=instance checks_type="Http" %} + +
+ +{% include 'cabotapp/_statuscheck_list_instance.html' with checks=instance.jenkins_status_checks.all instance=instance checks_type="Jenkins" %} + +
+ +{% include 'cabotapp/_statuscheck_list_instance.html' with checks=instance.icmp_status_checks.all instance=instance checks_type="ICMP" %} + +
+ +
+
+

+
+

Status check report

+
+
+
+
+
+ {{ report_form.instance }} + +
{{ report_form.checks }}
+
+
+
+
+ +
{{ report_form.date_from }}
+
+
+
+
+ +
{{ report_form.date_to }}
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+ +
+
+

+
+

Recovery instructions

+
+ {% if instance.hackpad_id %} +
+ +
+ {% else %} +
No hackpad configured
+ {% endif %} +
+
+ + +{% endblock content %} + +{% block js %} +{% load compress %} +{% load jsonify %} +{{ block.super }} + + +{% compress js %} + + + +{% endcompress %} +{% endblock js %} diff --git a/app/templates/cabotapp/instance_form.html b/app/templates/cabotapp/instance_form.html new file mode 100644 index 0000000..5da7d3c --- /dev/null +++ b/app/templates/cabotapp/instance_form.html @@ -0,0 +1,36 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+

{% if form.instance.id %}Edit instance{% else %}New instance{% endif %}

+
+
+
+
+ {% include "cabotapp/_base_form.html" %} +
+
+
+ + Back to dashboard +
+ {% if form.instance.id %} + + {% endif %} +
+
+
+{% endblock %} + +{% load compress %} +{% block js %} +{{ block.super }} +{% compress js %} + +{% endcompress %} +{% endblock %} diff --git a/app/templates/cabotapp/instance_list.html b/app/templates/cabotapp/instance_list.html new file mode 100644 index 0000000..6d4f850 --- /dev/null +++ b/app/templates/cabotapp/instance_list.html @@ -0,0 +1,63 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

+

Instances

+
+

+
+
+
+ {% if not instances %} + No instances + {% else %} + + + + + + + + + + + + {% for instance in instances %} + + + + + + + + {% endfor %} + +
NameOverallActive checksDisabled checks
+ {{instance.name}} + + {% if instance.alerts_enabled %}{{ instance.overall_status|lower|capfirst }}{% else %}Disabled{% endif %} + + {{ instance.active_status_checks.all.count }} + + {{ instance.inactive_status_checks.all.count }} + + + + +
+ {% endif %} +
+
+{% endblock content %} + +{% block js %} +{% load compress %} +{{ block.super }} +{% compress js %} + +{% endcompress %} +{% endblock js %} diff --git a/app/templates/cabotapp/service_detail.html b/app/templates/cabotapp/service_detail.html index 9e6781b..fed5863 100644 --- a/app/templates/cabotapp/service_detail.html +++ b/app/templates/cabotapp/service_detail.html @@ -55,18 +55,29 @@ +
+
+

+

Instances

+
+

+ +

+
+{% include 'cabotapp/_instance_list.html' with instances=service.instances.all %} +
+
+
+ +{% include 'cabotapp/_statuscheck_list_service.html' with checks=service.graphite_status_checks.all service=service checks_type="Graphite" %}
-{% include 'cabotapp/_statuscheck_list.html' with checks=service.graphite_status_checks.all service=service checks_type="Graphite" %} +{% include 'cabotapp/_statuscheck_list_service.html' with checks=service.http_status_checks.all service=service checks_type="Http" %}
-{% include 'cabotapp/_statuscheck_list.html' with checks=service.http_status_checks.all service=service checks_type="Http" %} - -
- -{% include 'cabotapp/_statuscheck_list.html' with checks=service.jenkins_status_checks.all service=service checks_type="Jenkins" %} +{% include 'cabotapp/_statuscheck_list_service.html' with checks=service.jenkins_status_checks.all service=service checks_type="Jenkins" %}
diff --git a/app/templates/cabotapp/statuscheck_detail.html b/app/templates/cabotapp/statuscheck_detail.html index 17114a5..84d4218 100644 --- a/app/templates/cabotapp/statuscheck_detail.html +++ b/app/templates/cabotapp/statuscheck_detail.html @@ -6,14 +6,14 @@
-

+

{{ check.name }}

{{ check.calculated_status|capfirst }}

{% if check.polymorphic_ctype.model == 'jenkinsstatuscheck' %} {% endif %} - +

@@ -81,4 +81,4 @@ {% endcompress %} -{% endblock js %} \ No newline at end of file +{% endblock js %} diff --git a/app/templates/cabotapp/statuscheck_list.html b/app/templates/cabotapp/statuscheck_list.html index 2f50b60..101821b 100644 --- a/app/templates/cabotapp/statuscheck_list.html +++ b/app/templates/cabotapp/statuscheck_list.html @@ -21,4 +21,4 @@ $('.sparktristate').sparkline('html', {type: 'tristate'}) {% endcompress %} -{% endblock js %} \ No newline at end of file +{% endblock js %} diff --git a/app/urls.py b/app/urls.py index c263b6e..70a25ae 100644 --- a/app/urls.py +++ b/app/urls.py @@ -3,10 +3,12 @@ from cabotapp.views import ( run_status_check, graphite_api_data, twiml_callback, checks_run_recently, GraphiteCheckCreateView, GraphiteCheckUpdateView, HttpCheckCreateView, HttpCheckUpdateView, + ICMPCheckCreateView, ICMPCheckUpdateView, JenkinsCheckCreateView, JenkinsCheckUpdateView, StatusCheckDeleteView, StatusCheckListView, StatusCheckDetailView, StatusCheckResultDetailView, StatusCheckReportView) -from cabotapp.views import (ServiceListView, ServiceDetailView, +from cabotapp.views import (InstanceListView, InstanceDetailView, + InstanceUpdateView, InstanceCreateView, InstanceDeleteView, ServiceListView, ServiceDetailView, ServiceUpdateView, ServiceCreateView, ServiceDeleteView, UserProfileUpdateView, ShiftListView, subscriptions) from django.contrib import admin @@ -43,6 +45,19 @@ urlpatterns = patterns('', url(r'^service/(?P\d+)/', view=ServiceDetailView.as_view(), name='service'), + url(r'^instances/', view=InstanceListView.as_view(), + name='instances'), + url(r'^instance/create/', view=InstanceCreateView.as_view(), + name='create-instance'), + url(r'^instance/update/(?P\d+)/', + view=InstanceUpdateView.as_view( + ), name='update-instance'), + url(r'^instance/delete/(?P\d+)/', + view=InstanceDeleteView.as_view( + ), name='delete-instance'), + url(r'^instance/(?P\d+)/', + view=InstanceDetailView.as_view(), name='instance'), + url(r'^checks/$', view=StatusCheckListView.as_view(), name='checks'), url(r'^check/run/(?P\d+)/', @@ -55,6 +70,11 @@ urlpatterns = patterns('', url(r'^checks/report/$', view=StatusCheckReportView.as_view(), name='checks-report'), + url(r'^icmpcheck/create/', view=ICMPCheckCreateView.as_view(), + name='create-icmp-check'), + url(r'^icmpcheck/update/(?P\d+)/', + view=ICMPCheckUpdateView.as_view( + ), name='update-icmp-check'), url(r'^graphitecheck/create/', view=GraphiteCheckCreateView.as_view( ), name='create-check'),