Merge remote-tracking branch 'upstream/master'

This commit is contained in:
romankor 2014-12-29 11:10:11 -08:00
commit 430f466473
7 changed files with 278 additions and 7 deletions

32
.travis.yml Normal file
View File

@ -0,0 +1,32 @@
language: python
python:
- "2.7"
services:
- redis-server
# install deps
install:
- sudo apt-get update
- sudo apt-get install -y python-software-properties
- sudo apt-get install gcc python-dev git python-pip libpq-dev npm rubygems
-
- sudo npm install -g coffee-script less@1.3 --registry http://registry.npmjs.org/
- gem install foreman
-
- sudo pip install virtualenv
- sudo virtualenv venv
- sudo ./venv/bin/pip install --upgrade setuptools
- sudo ./venv/bin/pip install --timeout=30 --exists-action=w -e . --no-use-wheel
# setup databases
before_script:
- cp conf/development.env.example conf/development.env
- cp conf/production.env.example conf/production.env
- . venv/bin/activate
- foreman run python manage.py syncdb --migrate --noinput
# tests
script:
- foreman run python manage.py test cabot

View File

@ -1,5 +1,6 @@
Cabot
=====
[![Build Status](https://img.shields.io/travis/arachnys/cabot/master.svg)](https://travis-ci.org/arachnys/cabot) [![docs](http://img.shields.io/badge/docs-latest-brightgreen.svg)](http://cabotapp.com/qs/quickstart.html)
Cabot is a free, open-source, self-hosted infrastructure monitoring platform that provides some of the best features of [PagerDuty](http://www.pagerduty.com), [Server Density](http://www.serverdensity.com), [Pingdom](http://www.pingdom.com) and [Nagios](http://www.nagios.org) without their cost and complexity. (Nagios, I'm mainly looking at you.)

View File

@ -23,7 +23,7 @@ Passing checks:{% for check in service.all_passing_checks %}
{% endif %}
"""
hipchat_template = "Service {{ service.name }} {% if service.overall_status == service.PASSING_STATUS %}is back to normal{% else %}reporting {{ service.overall_status }} status{% endif %}: {{ scheme }}://{{ host }}{% url 'service' pk=service.id %}. {% if service.overall_status != service.PASSING_STATUS %}Checks failing:{% for check in service.all_failing_checks %} {{ check.name }}{% if check.last_result.error %} ({{ check.last_result.error|safe }}){% endif %}{% endfor %}{% endif %}{% if alert %}{% for alias in users %} @{{ alias }}{% endfor %}{% endif %}"
hipchat_template = "Service {{ service.name }} {% if service.overall_status == service.PASSING_STATUS %}is back to normal{% else %}reporting {{ service.overall_status }} status{% endif %}: {{ scheme }}://{{ host }}{% url 'service' pk=service.id %}. {% if service.overall_status != service.PASSING_STATUS %}Checks failing:{% for check in service.all_failing_checks %}{% if check.check_category == 'Jenkins check' %}{% if check.last_result.error %} {{ check.name }} ({{ check.last_result.error|safe }}) {{jenkins_api}}job/{{ check.name }}/{{ check.last_result.job_number }}/console{% else %} {{ check.name }} {{jenkins_api}}/job/{{ check.name }}/{{check.last_result.job_number}}/console {% endif %}{% else %} {{ check.name }} {% if check.last_result.error %} ({{ check.last_result.error|safe }}){% endif %}{% endif %}{% endfor %}{% endif %}{% if alert %}{% for alias in users %} @{{ alias }}{% endfor %}{% endif %}"
sms_template = "Service {{ service.name }} {% if service.overall_status == service.PASSING_STATUS %}is back to normal{% else %}reporting {{ service.overall_status }} status{% endif %}: {{ scheme }}://{{ host }}{% url 'service' pk=service.id %}"
@ -31,7 +31,7 @@ telephone_template = "This is an urgent message from Arachnys monitoring. Servic
def send_alert(service, duty_officers=None):
users = service.users_to_notify.all()
users = service.users_to_notify.filter(is_active=True)
if service.email_alert:
try:
send_email_alert(service, users, duty_officers)
@ -103,6 +103,7 @@ def send_hipchat_alert(service, users, duty_officers):
'host': settings.WWW_HTTP_HOST,
'scheme': settings.WWW_SCHEME,
'alert': alert,
'jenkins_api': settings.JENKINS_API,
})
message = Template(hipchat_template).render(c)
_send_hipchat_alert(message, color=color, sender='Cabot/%s' % service.name)

View File

@ -22,6 +22,7 @@ def get_job_status(jobname):
resp = requests.get(endpoint, auth=auth, verify=True)
status = resp.json
ret['status_code'] = resp.status_code
ret['job_number'] = status['lastBuild'].get('number', None)
if status['color'].startswith('blue'):
ret['active'] = True
ret['succeeded'] = True

View File

@ -0,0 +1,172 @@
# -*- 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 field 'StatusCheckResult.job_number'
db.add_column('cabotapp_statuscheckresult', 'job_number',
self.gf('django.db.models.fields.PositiveIntegerField')(null=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'StatusCheckResult.job_number'
db.delete_column('cabotapp_statuscheckresult', 'job_number')
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'"}),
'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.instancestatussnapshot': {
'Meta': {'object_name': 'InstanceStatusSnapshot'},
'did_send_alert': ('django.db.models.fields.IntegerField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'snapshots'", 'to': "orm['cabotapp.Instance']"}),
'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'"}),
'time': ('django.db.models.fields.DateTimeField', [], {'db_index': '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']", 'null': 'True'}),
'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'}),
'job_number': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
'raw_data': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'succeeded': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'time': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
'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']

View File

@ -688,6 +688,7 @@ class JenkinsStatusCheck(StatusCheck):
try:
status = get_job_status(self.name)
active = status['active']
result.job_number = status['job_number']
if status['status_code'] == 404:
result.error = u'Job %s not found on Jenkins' % self.name
result.succeeded = False
@ -745,6 +746,9 @@ class StatusCheckResult(models.Model):
succeeded = models.BooleanField(default=False)
error = models.TextField(null=True)
# Jenkins specific
job_number = models.PositiveIntegerField(null=True)
def __unicode__(self):
return '%s: %s @%s' % (self.status, self.check.name, self.time)

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import requests
from django.conf import settings
from django.utils import timezone
from django.core.urlresolvers import reverse
from django.test import TestCase
@ -12,13 +13,14 @@ from rest_framework.test import APITestCase
from rest_framework.reverse import reverse as api_reverse
from cabot.cabotapp.models import (
GraphiteStatusCheck, JenkinsStatusCheck,
HttpStatusCheck, ICMPStatusCheck, Service, Instance, StatusCheckResult)
HttpStatusCheck, ICMPStatusCheck, Service, Instance,
StatusCheckResult, UserProfile)
from cabot.cabotapp.views import StatusCheckReportForm
from cabot.cabotapp.alert import (send_hipchat_alert, send_alert)
from mock import Mock, patch
from twilio import rest
from django.utils import timezone
from django.core import mail
from datetime import timedelta, date
from datetime import timedelta, date, datetime
import json
import os
import base64
@ -411,7 +413,8 @@ class TestAPI(LocalTestCase):
'sms_alert': False,
'telephone_alert': False,
'hackpad_id': None,
'id': 1
'id': 1,
'url': u''
},
],
'instance': [
@ -528,7 +531,8 @@ class TestAPI(LocalTestCase):
'sms_alert': False,
'telephone_alert': False,
'hackpad_id': None,
'id': 2
'id': 2,
'url': u'',
},
],
'instance': [
@ -744,3 +748,59 @@ class TestAPIFiltering(LocalTestCase):
[item['name'] for item in response.data],
self.expected_sort_names[::-1]
)
class TestAlerts(LocalTestCase):
def setUp(self):
super(TestAlerts, self).setUp()
self.user_profile = UserProfile.objects.create(
user = self.user,
hipchat_alias = "test_user_hipchat_alias",)
self.user_profile.save()
self.service.users_to_notify.add(self.user)
self.service.update_status()
def test_users_to_notify(self):
self.assertEqual(self.service.users_to_notify.all().count(), 1)
self.assertEqual(self.service.users_to_notify.get(pk=1).username, self.user.username)
@patch('cabot.cabotapp.models.send_alert')
def test_alert(self, fake_send_alert):
self.service.alert()
self.assertEqual(fake_send_alert.call_count, 1)
fake_send_alert.assert_called_with(self.service, duty_officers=[])
@patch('cabot.cabotapp.alert._send_hipchat_alert')
def test_inactive_users(self, fake_hipchat_alert):
self.user.is_active = True
self.user.save()
self.service.alert()
fake_hipchat_alert.assert_called_with(u'Service Service is back to normal: http://localhost/service/1/. @test_user_hipchat_alias', color='green', sender='Cabot/Service')
self.user.is_active = False
self.user.save()
self.service.alert()
fake_hipchat_alert.assert_called_with(u'Service Service is back to normal: http://localhost/service/1/. ', color='green', sender='Cabot/Service')
@patch('cabot.cabotapp.alert._send_hipchat_alert')
def test_normal_alert(self, fake_hipchat_alert):
self.service.overall_status = Service.PASSING_STATUS
self.service.old_overall_status = Service.ERROR_STATUS
self.service.save()
self.service.alert()
fake_hipchat_alert.assert_called_with(u'Service Service is back to normal: http://localhost/service/1/. @test_user_hipchat_alias', color='green', sender='Cabot/Service')
@patch('cabot.cabotapp.alert._send_hipchat_alert')
def test_failure_alert(self, fake_hipchat_alert):
# Most recent failed
self.service.overall_status = Service.CALCULATED_FAILING_STATUS
self.service.old_overall_status = Service.PASSING_STATUS
self.service.save()
self.service.alert()
fake_hipchat_alert.assert_called_with(u'Service Service reporting failing status: http://localhost/service/1/. Checks failing: @test_user_hipchat_alias', color='red', sender='Cabot/Service')