├── .coveragerc ├── .foreman ├── .gitignore ├── .travis.yml ├── CHANGES ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Pipfile ├── Procfile ├── Procfile.dev ├── README.md ├── Vagrantfile ├── bin ├── activate ├── activate.fish ├── build-app └── test_with_coverage ├── cabot ├── __init__.py ├── cabot_config.py ├── cabotapp │ ├── __init__.py │ ├── admin.py │ ├── alert.py │ ├── apps.py │ ├── calendar.py │ ├── graphite.py │ ├── jenkins.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20170131_1537.py │ │ ├── 0003_auto_20170201_1045.py │ │ ├── 0004_auto_20170802_1327.py │ │ ├── 0005_auto_20170818_1202.py │ │ ├── 0006_auto_20170821_1000.py │ │ ├── 0007_statuscheckresult_consecutive_failures.py │ │ └── __init__.py │ ├── models │ │ ├── __init__.py │ │ ├── base.py │ │ └── jenkins_check_plugin.py │ ├── tasks.py │ ├── templatetags │ │ ├── __init__.py │ │ └── extra.py │ ├── tests │ │ ├── __init__.py │ │ ├── fixtures │ │ │ ├── __init__.py │ │ │ ├── cabot_check_skeleton │ │ │ │ ├── __init__.py │ │ │ │ └── plugin.py │ │ │ ├── gcal_response.ics │ │ │ ├── graphite_avg_response.json │ │ │ ├── graphite_null_response.json │ │ │ ├── graphite_response.json │ │ │ ├── http_response.html │ │ │ ├── recurring_response.ics │ │ │ ├── recurring_response_complex.ics │ │ │ └── recurring_response_notz.ics │ │ ├── test_plugin_settings.py │ │ ├── test_setup.py │ │ ├── test_urlprefix.py │ │ ├── tests_basic.py │ │ ├── tests_icmp_check.py │ │ └── tests_jenkins.py │ ├── utils.py │ └── views.py ├── celery.py ├── celeryconfig.py ├── context_processors.py ├── entrypoint.py ├── rest_urls.py ├── settings.py ├── settings_ldap.py ├── settings_utils.py ├── static │ ├── 404.html │ ├── 500.html │ ├── 502.html │ ├── 503.html │ ├── 504.html │ ├── arachnys │ │ ├── css │ │ │ ├── base.less │ │ │ ├── graph.css │ │ │ └── morris.css │ │ ├── img │ │ │ ├── favicon.ico │ │ │ ├── icon_48x48.png │ │ │ └── icon_96x96.png │ │ └── js │ │ │ ├── d3.js │ │ │ ├── morris.js │ │ │ ├── raphael.js │ │ │ └── rickshaw.js │ ├── bootstrap │ │ ├── css │ │ │ ├── bootstrap.css │ │ │ └── dashboard.css │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ └── js │ │ │ ├── bootstrap.js │ │ │ └── jquery-1.10.2.js │ ├── favicon.ico │ ├── robots.txt │ └── theme │ │ ├── css │ │ ├── bootstrap-chosen.css │ │ ├── bootstrap-datatables.min.css │ │ ├── bootstrap-responsive.css │ │ ├── bootstrap.css │ │ ├── chosen-sprite.png │ │ ├── chosen-sprite@2x.png │ │ ├── chosen.css │ │ └── jquery-ui-1.8.21.custom.css │ │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── img │ │ ├── animated-overlay.gif │ │ ├── arrows-active.png │ │ ├── arrows-normal.png │ │ ├── bg-input-focus.png │ │ ├── bg-input.png │ │ ├── bg-login.jpg │ │ ├── bg.jpg │ │ ├── buttons.gif │ │ ├── calendar.gif │ │ ├── chat-left.png │ │ ├── chat-right.png │ │ ├── chosen-sprite.png │ │ ├── close-button-white.png │ │ ├── close-button.png │ │ ├── crop.gif │ │ ├── dbg.jpg │ │ ├── dialogs.png │ │ ├── favicon.ico │ │ ├── glyphicons-halflings-red.png │ │ ├── glyphicons-halflings-white.png │ │ ├── glyphicons-halflings.png │ │ ├── i_16_radio.png │ │ ├── icons-big.png │ │ ├── icons-small.png │ │ ├── logo.png │ │ ├── logo20.png │ │ ├── progress.gif │ │ ├── quicklook-bg.png │ │ ├── quicklook-icons.png │ │ ├── resize.png │ │ ├── spinner-mini.gif │ │ ├── sprite.png │ │ ├── toolbar.gif │ │ ├── toolbar.png │ │ ├── ui-bg_flat_0_aaaaaa_40x100.png │ │ ├── ui-bg_flat_75_ffffff_40x100.png │ │ ├── ui-bg_glass_55_fbf9ee_1x400.png │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ ├── ui-bg_glass_75_dadada_1x400.png │ │ ├── ui-bg_glass_75_e6e6e6_1x400.png │ │ ├── ui-bg_glass_95_fef1ec_1x400.png │ │ ├── ui-bg_highlight-soft_75_cccccc_1x100.png │ │ ├── ui-icons_222222_256x240.png │ │ ├── ui-icons_2e83ff_256x240.png │ │ ├── ui-icons_454545_256x240.png │ │ ├── ui-icons_888888_256x240.png │ │ └── ui-icons_cd0a0a_256x240.png │ │ └── js │ │ ├── bootstrap.js │ │ ├── chosen.jquery.js │ │ ├── custom.js │ │ ├── jquery-ui.js │ │ ├── jquery.dataTables.bootstrap.min.js │ │ ├── jquery.dataTables.min.js │ │ ├── jquery.sparkline.min.js │ │ ├── jquery.ui.autocomplete.js │ │ ├── jquery.ui.core.js │ │ └── jquery.ui.position.js ├── templates │ ├── 404.html │ ├── 500.html │ ├── base.html │ ├── base_public.html │ ├── cabotapp │ │ ├── _base_form.html │ │ ├── _instance_list.html │ │ ├── _service_list.html │ │ ├── _service_public_list.html │ │ ├── _statuscheck_list.html │ │ ├── about.html │ │ ├── alertpluginuserdata_form.html │ │ ├── instance_confirm_delete.html │ │ ├── instance_detail.html │ │ ├── instance_form.html │ │ ├── instance_list.html │ │ ├── plugin_settings_form.html │ │ ├── service_confirm_delete.html │ │ ├── service_detail.html │ │ ├── service_form.html │ │ ├── service_list.html │ │ ├── service_public_list.html │ │ ├── setup.html │ │ ├── shift_list.html │ │ ├── statuscheck_confirm_delete.html │ │ ├── statuscheck_detail.html │ │ ├── statuscheck_form.html │ │ ├── statuscheck_list.html │ │ ├── statuscheck_report.html │ │ ├── statuscheckresult_detail.html │ │ └── subscriptions.html │ └── registration │ │ ├── login.html │ │ ├── logout.html │ │ └── social_auth.html ├── urls.py ├── version.py └── wsgi.py ├── conf ├── default.env ├── development.env.example ├── production.env.example └── test.env ├── docker-compose-base.yml ├── docker-compose-test.yml ├── docker-compose.yml ├── docker-entrypoint.sh ├── example_local_config.yml ├── gunicorn.conf ├── makemigrations ├── manage.py ├── requirements-dev.txt ├── requirements-plugins.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── setup_dev.sh ├── tox.ini └── upstart └── process.conf.erb /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | plugins = 4 | django_coverage_plugin 5 | 6 | omit = *migrations* 7 | -------------------------------------------------------------------------------- /.foreman: -------------------------------------------------------------------------------- 1 | # vi: set ft=yaml : 2 | 3 | procfile: Procfile.dev 4 | env: conf/development.env 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dotcloud/* 2 | dev.db 3 | venv/* 4 | backups/* 5 | static/ 6 | cabot/.collectstatic/ 7 | node_modules/* 8 | .python-eggs/* 9 | cabot.egg-info 10 | cabot/static/ 11 | .env 12 | .DS_Store 13 | celerybeat-schedule 14 | *.pyc 15 | *.swp 16 | *.orig 17 | .vagrant 18 | conf/*.env 19 | dist/ 20 | local_config.yml 21 | build/ 22 | 23 | .idea 24 | Pipfile.lock 25 | .tox/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | 5 | before_install: 6 | - sudo pip install tox 7 | 8 | # setup databases 9 | before_script: 10 | - cp conf/development.env.example conf/development.env 11 | - docker-compose build 12 | 13 | script: 14 | - tox 15 | - docker-compose -f docker-compose-test.yml run --rm --entrypoint bin/test_with_coverage test -v2 16 | - git checkout $(git describe --abbrev=0 --tags `git describe --tags`^) && docker-compose build web 17 | - docker-compose run --rm web true 18 | - git checkout - && docker-compose build web 19 | - docker-compose run --rm web true 20 | 21 | after_success: 22 | - sudo pip install codecov 23 | - sudo pip install django_coverage_plugin==1.4.2 24 | - codecov 25 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Version 0.11.16 2 | --------------- 3 | 4 | * Upgrade celery and kombu 5 | 6 | Version 0.11.15 7 | --------------- 8 | 9 | * Fix dockerfile 10 | * Add support for changing default celery queue name (using CELERY_DEFAULT_QUEUE env variable) 11 | 12 | Version 0.11.14 13 | --------------- 14 | 15 | * Add support for celery SQS backend 16 | 17 | Version 0.11.13 18 | --------------- 19 | 20 | * Fix bug where jenkins checks were always passing 21 | * Reduce Arachnys branding a bit 22 | * Fix 404 page for logged out users 23 | * Style forms with django-bootstrap-form 24 | * [[#605](https://github.com/arachnys/cabot/issues/605)] Fix http check forms auto-filling username/password 25 | * [[#607](https://github.com/arachnys/cabot/issues/607)] Fix checks for websites service non utf-8 content 26 | 27 | Version 0.11.12 28 | --------------- 29 | 30 | * Upgrade django to 1.11.11 31 | * Debounce JenkinsCheck on the number of job failures 32 | - Previously it would fail after cabot checked the jenkins api N times, even if the jenkins job had only failed once 33 | 34 | Version 0.11.11 35 | --------------- 36 | 37 | * Fix /api/oncall endpoint not working with basic auth 38 | 39 | Version 0.11.10 40 | --------------- 41 | 42 | * Add /api/oncall endpoint 43 | 44 | Version 0.11.9 45 | -------------- 46 | 47 | * Fix issue where Jenkins environment variables were required on first launch 48 | 49 | Version 0.11.8 50 | -------------- 51 | 52 | * Bump cabot-alert-slack to 0.8.2 53 | * Update LDAP dependencies 54 | * Add ENABLE_SUBSCRIPTION and ENABLE_DUTY_ROTA options 55 | * [[#556](https://github.com/arachnys/cabot/issues/556)] Fix issue with HttpStatusCheck with unicode content 56 | 57 | Version 0.11.7 58 | -------------- 59 | 60 | * Fix check plugins not displaying checks correctly on service details page 61 | 62 | Version 0.11.6 63 | -------------- 64 | 65 | * Add cloudwatch check plugin to dockerfile by default 66 | - Can be enabled by adding "cabot_check_cloudwatch" to CABOT_PLUGINS_ENABLED 67 | 68 | Version 0.11.5 69 | -------------- 70 | 71 | * Fix multiple jenkins configs not working properly 72 | - Due to caching on the client, the first config to be checked would always be used 73 | 74 | Version 0.11.4 75 | -------------- 76 | 77 | * Switch from jenkinsapi to python-jenkins 78 | - Fixes performance regression introduced in 0.11 79 | 80 | Version 0.11.3 81 | -------------- 82 | 83 | * [[#551](https://github.com/arachnys/cabot/issues/551)] Fix in-progress jenkins jobs being marked as failing 84 | 85 | Version 0.11.2 86 | -------------- 87 | 88 | * Fix pypi source distribution missing requirements for setup.py 89 | 90 | Version 0.11.1 91 | -------------- 92 | 93 | * Fix migration disassociating checks from services/instances 94 | * Fix migration requiring jenkins environment variables are set 95 | * Reduce time to store old check results to 7 days 96 | - Currently stores for 2 months, but there's no actual way to view the old data. 97 | 98 | Version 0.11.0 99 | -------------- 100 | 101 | *** BROKEN RELEASE - MIGRATIONS DON'T WORK CORRECTLY *** 102 | 103 | * Jenkins support: 104 | - Fail Jenkins checks when job is unknown 105 | - Use [jenkinsapi](https://pypi.python.org/pypi/jenkinsapi) to talk to Jenkins 106 | - Add option to specify multiple Jenkins backends 107 | > NOTE: This update will delete any recent status check results for jenkins checks 108 | * Add view for public services 109 | * Add support for Google OAuth login 110 | * Add ability to add custom check plugins 111 | - See https://gitlab.com/as-public/cabot-check-skeleton for an example 112 | * Remove deprecated Fabfile and Shell scripts 113 | 114 | Version 0.10.8 115 | -------------- 116 | 117 | * Update slack alert to 0.8.1 118 | - fixes names not linking 119 | - only shows the acknowledge button if "SLACK_INTERACTIVE_MESSAGES" is set 120 | - (The feature only works if set up correctly on the slack end) 121 | * Update to django 1.11 (with working django-polymorphic this time) 122 | 123 | Version 0.10.7 124 | -------------- 125 | 126 | * Update slack alert plugin 127 | - Now shows an "acknowledge" button within slack 128 | * Fix alert tests not triggering if: 129 | - A user had acknowledged working on the service 130 | - A legitimate alert had been sent recently 131 | * Add support for GitHub OAuth logins 132 | - See http://cabotapp.com/use/users.html 133 | 134 | Version 0.10.6 135 | -------------- 136 | 137 | * Fix plugin urls being overridden by plugin settings urls 138 | - This fixes e.g. the twilio callback url not working 139 | * Fix profile settings sidebar links not working 140 | 141 | Version 0.10.5 142 | -------------- 143 | 144 | * Fix bug which caused status graphs to sometimes not render 145 | * Fix issue with complex recurring calendar - `'vDDDLists' object is not iterable` 146 | * Fix css regression in logo/title 147 | 148 | Version 0.10.4 149 | -------------- 150 | 151 | * Fix basic auth passwords getting reset when editing checks 152 | * Fix plugin alert tests alerting the current duty officer 153 | - They should now always alert only the user that runs the test 154 | 155 | Version 0.10.3 156 | -------------- 157 | 158 | * Add plugin settings views with the ability to test alerts. 159 | * Allow user filter for LDAP to be configured 160 | - Set the AUTH_LDAP_USER_FILTER setting to change it (defaults to "(uid=%(user)s)") 161 | * Update cabot-alert-hipchat plugin to 2.0.2 162 | - Fixes bug when both HIPCHAT_URL and HIPCHAT_DOMAIN were set 163 | 164 | Version 0.10.2 165 | -------------- 166 | 167 | * Update cabot-alert-hipchat plugin to 2.0.1 168 | - Supports Hipchat API v2 169 | - If HIPCHAT_URL is set, it will use the old v1 api 170 | - Use HIPCHAT_DOMAIN for custom hipchat v2 deployments 171 | * Add interactive api docs (using djangorestframework 3.6) at /docs 172 | 173 | Version 0.10.1 174 | -------------- 175 | 176 | * [BUGFIX] Update cabot_alert_twilio to 1.3.1 177 | - 1.3.0 was still broken on django 1.10 178 | 179 | Version 0.10.0 180 | -------------- 181 | 182 | * Add feedback notifications when updated profile 183 | * Automatically reload plugins after migrating 184 | * Add cabot_alert_slack as default plugin 185 | * Upgrade to Django 1.10 186 | * Upgrade to Celery 4 187 | 188 | Version 0.9.2 189 | ------------- 190 | 191 | * Add /about endpoint 192 | * Fix rota bug when ical had no timezone 193 | * Add User Profile settings link to user dropdown 194 | 195 | Version 0.9.1 196 | ------------- 197 | 198 | * Update cabot-alert-twilio to 1.3.0 to work on django 1.10 199 | * Fix Alert preferences form breaking on django 1.8 200 | * Add `cabot` executable instead of using python manage.py (for e.g. migrating) 201 | 202 | Version 0.9.0 203 | ------------- 204 | 205 | * Upgrade to Django 1.9 206 | 207 | Version 0.8.7 208 | ------------- 209 | 210 | * Fix Alert preferences form breaking on django 1.8 211 | 212 | Version 0.8.6 213 | ------------- 214 | 215 | * Add first time setup page 216 | * Remove create_cabot_superuser management command (redundant with first time setup) 217 | 218 | Version 0.8.5 219 | ------------- 220 | 221 | * More severe alerts should trigger even if a less severe alert was recently sent 222 | * Update production.env.example email settings 223 | * Convert environment vars to boolean nicely 224 | 225 | > Note: You may have to update your settings if they contain invalid boolean values 226 | 227 | Version 0.8.4 228 | ------------- 229 | 230 | * Fix setup.py packaging 231 | * Use whitenoise to serve static files 232 | 233 | > Note: You may have to update your webserver settings for static files to work properly 234 | 235 | Version 0.8.3 236 | ------------- 237 | 238 | * BUG: Add missing context processor 239 | 240 | Version 0.8.2 241 | ------------- 242 | 243 | * Remove django-smtp-ssl dependency 244 | * Build docker image from alpine 245 | * Refactor docker-compose files 246 | * Fix db_clean task failing on large results tables 247 | * Wait for docker containers to start in docker-entrypoint.sh 248 | * Update CABOT_PLUGINS_ENABLED to compatible plugin versions 249 | * Automatically initialise database, assets and superuser on docker container start 250 | 251 | Version 0.8.1 252 | ------------- 253 | 254 | * Fix all workers running celery beat 255 | * Update django-compressor to run on django 1.8 256 | * Fix typo in url testcase 257 | * Update wsgi.py to work with django 1.8 258 | 259 | Version 0.8.0 260 | ------------- 261 | 262 | * Upgraded to Django 1.8 263 | 264 | Version 0.7.0 265 | ------------- 266 | 267 | * Upgraded to Django 1.7 268 | 269 | Version 0.6.0 270 | ------------- 271 | 272 | * Versioning Introduced. 273 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4-alpine 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN mkdir /code 6 | 7 | WORKDIR /code 8 | 9 | RUN apk add --no-cache \ 10 | python-dev \ 11 | py-pip \ 12 | postgresql-dev \ 13 | gcc \ 14 | curl \ 15 | curl-dev \ 16 | libcurl \ 17 | musl-dev \ 18 | libffi-dev \ 19 | openldap-dev \ 20 | ca-certificates \ 21 | bash 22 | 23 | RUN npm config set unsafe-perm true 24 | RUN npm install -g \ 25 | --registry http://registry.npmjs.org/ \ 26 | coffee-script \ 27 | less@1.3 28 | 29 | RUN pip install --upgrade pip 30 | 31 | COPY requirements.txt ./ 32 | RUN pip install --no-cache-dir -r requirements.txt 33 | 34 | COPY requirements-dev.txt ./ 35 | RUN pip install --no-cache-dir -r requirements-dev.txt 36 | 37 | COPY requirements-plugins.txt ./ 38 | RUN pip install --no-cache-dir -r requirements-plugins.txt 39 | 40 | ADD . /code/ 41 | 42 | ENTRYPOINT ["./docker-entrypoint.sh"] 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Arachnys Information Services Ltd and individual contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include cabot/.collectstatic * 2 | recursive-include cabot/templates * 3 | include requirements.txt 4 | include requirements-plugins.txt 5 | include requirements-dev.txt 6 | include README.md 7 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | 5 | [packages] 6 | amqp = ">=2.1.4" 7 | celery = ">=4,<5" 8 | djangorestframework = "*" 9 | django-jsonify = "*" 10 | django-filter = "*" 11 | django-auth-ldap = "*" 12 | anyjson = "*" 13 | dj-database-url = "*" 14 | freezegun = "*" 15 | gevent = "*" 16 | gunicorn = "*" 17 | icalendar = "*" 18 | psycopg2 = "*" 19 | python-dateutil = "*" 20 | pytz = "*" 21 | redis = "*" 22 | requests = "*" 23 | twilio = ">=5,<6" 24 | whitenoise = "*" 25 | coreapi = "*" 26 | Django = ">=1.11,<2" 27 | django_polymorphic = "*" 28 | django_compressor = "*" 29 | Markdown = "*" 30 | Pygments = "*" 31 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn cabot.wsgi:application --config gunicorn.conf 2 | celery: celery worker -A cabot --loglevel=INFO --concurrency=16 -Ofair 3 | beat: celery beat -A cabot --loglevel=INFO 4 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: python manage.py runserver 0.0.0.0:$PORT 2 | celery: celery -A cabot worker --loglevel=DEBUG -c 8 -Ofair 3 | beat: celery -A cabot beat --loglevel=DEBUG 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cabot 2 | ===== 3 | [![Build Status](https://travis-ci.org/arachnys/cabot.svg?branch=master)](https://travis-ci.org/arachnys/cabot) 4 | [![PyPI version](https://badge.fury.io/py/cabot.svg)](https://badge.fury.io/py/cabot) 5 | [![Coverage Status](https://codecov.io/github/arachnys/cabot/coverage.svg?branch=master)](https://codecov.io/github/arachnys/cabot?branch=master) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | [![Gitter](https://img.shields.io/gitter/room/arachnys/cabot.svg)](https://gitter.im/arachnys/cabot) 8 | 9 | ## Maintainers wanted 10 | 11 | **Cabot is stable and used by hundreds of companies and individuals in production, but it is not actively maintained. We would like to hand over maintenance of the project to one or more responsible and experienced maintainers. Please email cabot@arachnys.com with some information about yourself (github profile and/or CV) if you are interested.** 12 | 13 | ## Why choose Cabot 14 | 15 | 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.) 16 | 17 | It provides a web interface that allows you to monitor services (e.g. "Stage Redis server", "Production ElasticSearch cluster") and send telephone, sms or hipchat/email alerts to your on-duty team if those services start misbehaving or go down - all without writing a line of code. Best of all, you can use data that you're already pushing to Graphite/statsd to generate alerts, rather than implementing and maintaining a whole new system of data collectors. 18 | 19 | You can alert based on: 20 | 21 | * Metrics from [Graphite](https://github.com/graphite-project/graphite-web) 22 | * Status code and response content of web endpoints 23 | * [Jenkins](http://jenkins-ci.org) build statuses 24 | 25 | We built Cabot as a Christmas project at [Arachnys](https://www.arachnys.com) because we couldn't wrap our heads around Nagios, and nothing else out there seemed to fit our use case. We're open-sourcing it in the hope that others find it useful. 26 | 27 | Cabot is written in Python and uses [Django](https://www.djangoproject.com/), [Bootstrap](http://getbootstrap.com/), [Font Awesome](http://fontawesome.io) and a whole host of other goodies under the hood. 28 | 29 | ## Screenshots 30 | 31 | ### Services dashboard 32 | 33 | ![Services dashboard](https://dl.dropboxusercontent.com/s/cgpxe3929is2uar/cabot-service-dashboard.png?dl=1&token_hash=AAHrlDisUzWRxpg892LhlKQWFRNSkZKD7l_zdSxND-YKhw) 34 | 35 | ### Single service overview 36 | 37 | ![Individual service overview](https://dl.dropboxusercontent.com/s/541p0kbq3pwone6/cabot-service-status.png?dl=1&token_hash=AAGpSI6lyHm3-xCQSFOyyZ_SkJOzfdMIxfa-gYgCVS25pw) 38 | 39 | ## Quickstart 40 | 41 | Using Docker: Deploy in 5 minutes or less using [official quickstart guide at cabotapp.com](http://cabotapp.com/qs/quickstart.html). (See also https://hub.docker.com/r/cabotapp/cabot/) 42 | 43 | ## How it works 44 | 45 | Docs have moved to [cabotapp.com](http://cabotapp.com) 46 | 47 | Sections: 48 | 49 | * [Configuration](http://cabotapp.com/use/configuration.html) 50 | * [Deployment](http://cabotapp.com/use/deployment.html) 51 | * [Services](http://cabotapp.com/use/services.html) 52 | * [Graphite checks](http://cabotapp.com/use/graphite-checks.html) 53 | * [Jenkins checks](http://cabotapp.com/use/jenkins-checks.html) 54 | * [HTTP checks](http://cabotapp.com/use/http-checks.html) 55 | * [Alerting](http://cabotapp.com/use/alerting.html) 56 | * [Users](http://cabotapp.com/use/users.html) 57 | * [Rota](http://cabotapp.com/use/rota.html) 58 | 59 | For those who want to contribute: 60 | 61 | * [Help develop](http://cabotapp.com/dev/get-started.html) 62 | * [Contribute code](http://cabotapp.com/dev/contribute-code.html) 63 | 64 | ## FAQ 65 | 66 | ### Why "Cabot"? 67 | 68 | My dog is called Cabot and he loves monitoring things. Mainly the presence of food in his immediate surroundings, or perhaps the frequency of squirrel visits to our garden. He also barks loudly to alert us on certain events (e.g. the postman coming to the door). 69 | 70 | ![Cabot watching... something](https://dl.dropboxusercontent.com/sc/w0k0185wur929la/RN6X-PkZIl/0?dl=1&token_hash=AAEvyK-dMHsvMPwMsx89tSHXsUlMC8WN_fIu_H1Vo9wxWA) 71 | 72 | It's just a lucky coincidence that his name sounds like he could be an automation tool. 73 | 74 | ## API 75 | 76 | The API has automatically generated documentation available by browsing https://cabot.yourcompany.com/api. The browsable documentation displays example GET requests and lists other allowed HTTP methods. 77 | 78 | To view individual items, append the item `id` to the url. For example, to view `graphite_check` 1, browse: 79 | ``` 80 | /api/graphite_checks/1/ 81 | ``` 82 | 83 | ### Authentication 84 | 85 | The API allows HTTP basic auth using standard Django usernames and passwords as well as session authentication (by submitting the login form on the login page). The API similarly uses standard Django permissions to allow and deny API access. 86 | 87 | All resources are GETable by any authenticated user, but individual permissions must be granted for POST, PUT, and other write methods. 88 | 89 | As an example, for POST access to all `status_check` subclasses, add the following permissions: 90 | ``` 91 | cabotapp | status check | Can add graphite status check 92 | cabotapp | status check | Can add http status check 93 | cabotapp | status check | Can add icmp status check 94 | cabotapp | status check | Can add jenkins status check 95 | ``` 96 | 97 | Access the Django admin page at https://cabot.yourcompany.com/admin to add/remove users, change user permissions, add/remove groups for group-based permission control, and change group permissions. 98 | 99 | ### Sorting and Filtering 100 | 101 | Sorting and filtering can be used by both REST clients and on the browsable API. All fields visible in the browsable API can be used for filtering and sorting. 102 | 103 | Get all `jenkins_checks` with debounce enabled and CRITICAL importance: 104 | ``` 105 | https://cabot.yourcompany.com/api/jenkins_checks/?debounce=1&importance=CRITICAL 106 | ``` 107 | 108 | Sort `graphite_checks` by `name` field, ascending: 109 | ``` 110 | https://cabot.yourcompany.com/api/graphite_checks/?ordering=name 111 | ``` 112 | 113 | Sort by `name` field, descending: 114 | ``` 115 | https://cabot.yourcompany.com/api/graphite_checks/?ordering=-name 116 | ``` 117 | 118 | Other (non-Cabot specific) examples are available in the [Django REST Framework](http://www.django-rest-framework.org/api-guide/filtering#djangofilterbackend) documentation. 119 | 120 | ## License 121 | 122 | See `LICENSE` file in this repo. 123 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | require 'yaml' 4 | 5 | # Load local config overrides 6 | local_config = File.file?("local_config.yml") ? YAML.load(File.read("local_config.yml")) : {} 7 | 8 | Vagrant::configure("2") do |config| 9 | # All Vagrant configuration is done here. The most common configuration 10 | # options are documented and commented below. For a complete reference, 11 | # please see the online documentation at vagrantup.com. 12 | 13 | # Every Vagrant virtual environment requires a box to build off of. 14 | config.vm.box = local_config["box"] || "hashicorp/precise64" 15 | 16 | # Virtualbox 17 | config.vm.provider "virtualbox" do |vb| 18 | vb.customize [ 19 | "modifyvm", :id, 20 | "--memory", local_config['ram'] || "1024", 21 | "--cpus", local_config['cpu'] || 1, 22 | "--ioapic", "on", 23 | ] 24 | end 25 | 26 | #vmware_fusion 27 | config.vm.provider "vmware_fusion" do |v| 28 | v.vmx["memsize"] = local_config['ram'] || "1024" 29 | v.vmx["numvcpus"] = local_config['cpu'] || 1 30 | end 31 | 32 | # Boot with a GUI so you can see the screen. (Default is headless) 33 | # config.vm.boot_mode = :gui 34 | 35 | # Assign this VM to a host-only network IP, allowing you to access it 36 | # via the IP. Host-only networks can talk to the host machine as well as 37 | # any other machines on the same network, but cannot be accessed (through this 38 | # network interface) by any external networks. 39 | config.vm.network "forwarded_port", guest: 5001, host: 5001 40 | 41 | # Share an additional folder to the guest VM. The first argument is 42 | # an identifier, the second is the path on the guest to mount the 43 | # folder, and the third is the path on the host to the actual folder. 44 | config.vm.synced_folder "./", "/vagrant", create: true 45 | 46 | # Provision the development environment 47 | config.vm.provision :shell do |shell| 48 | shell.inline = 'sudo /vagrant/bin/provision' 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /bin/activate: -------------------------------------------------------------------------------- 1 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 2 | export PATH=$PATH:$DIR/../bin:$DIR/../app 3 | export PYTHONPATH=$PYTHONPATH:$DIR/../app 4 | -------------------------------------------------------------------------------- /bin/activate.fish: -------------------------------------------------------------------------------- 1 | # fix broken locale 2 | if not python -c 'import locale; locale.getdefaultlocale();' >/dev/null ^&1 3 | set -gx LANG en_US.UTF-8 4 | set -gx LC_ALL en_US.UTF-8 5 | end 6 | 7 | # set paths 8 | set DIR (dirname (status -f)) 9 | set -gx PATH $PATH $DIR/../bin $DIR/../app 10 | set -gx PYTHONPATH $PYTHONPATH $DIR/../app 11 | -------------------------------------------------------------------------------- /bin/build-app: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | python manage.py migrate 4 | python manage.py collectstatic --noinput 5 | # python manage.py compress 6 | -------------------------------------------------------------------------------- /bin/test_with_coverage: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | 4 | output_dir='test-results' 5 | mkdir -p $output_dir 6 | 7 | ./manage.py collectstatic --no-input 8 | TEMPLATE_DEBUG=True coverage run --source="./cabot/" manage.py test $@ 9 | status=$? 10 | 11 | coverage report --omit="cabot/cabotapp/tests*" 12 | coverage xml --omit="cabot/cabotapp/tests*" -o $output_dir/coverage.xml 13 | coverage html --omit="cabot/cabotapp/tests*" -d $output_dir/htmlcov/ 14 | 15 | exit $status 16 | -------------------------------------------------------------------------------- /cabot/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | # This will make sure the app is always imported when 4 | # Django starts so that shared_task will use this app. 5 | from .celery import app as celery_app 6 | 7 | from .version import version 8 | -------------------------------------------------------------------------------- /cabot/cabot_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Credentials for Graphite server to monitor 4 | GRAPHITE_API = os.environ.get('GRAPHITE_API') 5 | GRAPHITE_USER = os.environ.get('GRAPHITE_USER') 6 | GRAPHITE_PASS = os.environ.get('GRAPHITE_PASS') 7 | GRAPHITE_FROM = os.getenv('GRAPHITE_FROM', '-10minute') 8 | 9 | # Credentials for Jenkins server to monitor 10 | JENKINS_API = os.environ.get('JENKINS_API') 11 | JENKINS_USER = os.environ.get('JENKINS_USER') 12 | JENKINS_PASS = os.environ.get('JENKINS_PASS') 13 | 14 | # Point at a public calendar you want to use to schedule a duty rota 15 | CALENDAR_ICAL_URL = os.environ.get('CALENDAR_ICAL_URL') 16 | 17 | # So that links back to the Cabot instance display correctly 18 | WWW_HTTP_HOST = os.environ.get('WWW_HTTP_HOST') 19 | WWW_SCHEME = os.environ.get('WWW_SCHEME', "https") 20 | 21 | HTTP_USER_AGENT = os.environ.get('HTTP_USER_AGENT', 'Cabot') 22 | 23 | # How often should alerts be sent for important failures? 24 | ALERT_INTERVAL = int(os.environ.get('ALERT_INTERVAL', 10)) 25 | 26 | # How often should notifications be sent for less important issues? 27 | NOTIFICATION_INTERVAL = int(os.environ.get('NOTIFICATION_INTERVAL', 120)) 28 | 29 | # How long should an acknowledgement silence alerts for? 30 | ACKNOWLEDGEMENT_EXPIRY = int(os.environ.get('ACKNOWLEDGEMENT_EXPIRY', 20)) 31 | 32 | # Default plugins are used if the user has not specified. 33 | CABOT_PLUGINS_ENABLED = os.environ.get('CABOT_PLUGINS_ENABLED', 'cabot_alert_hipchat,cabot_alert_twilio,cabot_alert_email') 34 | -------------------------------------------------------------------------------- /cabot/cabotapp/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'cabot.cabotapp.apps.CabotappConfig' 2 | -------------------------------------------------------------------------------- /cabot/cabotapp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from polymorphic.admin import (PolymorphicChildModelAdmin, 3 | PolymorphicParentModelAdmin) 4 | 5 | from .alert import AlertPlugin, AlertPluginUserData 6 | from .models import (AlertAcknowledgement, Instance, JenkinsConfig, Service, 7 | ServiceStatusSnapshot, Shift, StatusCheck, 8 | StatusCheckResult, UserProfile) 9 | 10 | 11 | class StatusCheckAdmin(PolymorphicParentModelAdmin): 12 | base_model = StatusCheck 13 | child_models = StatusCheck.__subclasses__() 14 | 15 | 16 | class ChildStatusCheckAdmin(PolymorphicChildModelAdmin): 17 | base_model = StatusCheck 18 | 19 | 20 | for child_status_check in StatusCheck.__subclasses__(): 21 | admin.site.register(child_status_check, ChildStatusCheckAdmin) 22 | 23 | admin.site.register(UserProfile) 24 | admin.site.register(Shift) 25 | admin.site.register(Service) 26 | admin.site.register(ServiceStatusSnapshot) 27 | admin.site.register(StatusCheck, StatusCheckAdmin) 28 | admin.site.register(StatusCheckResult) 29 | admin.site.register(Instance) 30 | admin.site.register(AlertPlugin) 31 | admin.site.register(AlertPluginUserData) 32 | admin.site.register(AlertAcknowledgement) 33 | admin.site.register(JenkinsConfig) 34 | -------------------------------------------------------------------------------- /cabot/cabotapp/alert.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.db import models 4 | 5 | 6 | from polymorphic.models import PolymorphicModel 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class AlertPlugin(PolymorphicModel): 12 | title = models.CharField(max_length=30, unique=True, blank=False, editable=False) 13 | enabled = models.BooleanField(default=True) 14 | 15 | author = None 16 | 17 | def __unicode__(self): 18 | return u'%s' % (self.title) 19 | 20 | def _send_alert(self, service, users, duty_officers): 21 | """ 22 | To allow easily monkey patching in hooks for all alerts. 23 | e.g. mocking send_alert for all plugins in testing 24 | """ 25 | return self.send_alert(service, users, duty_officers) 26 | 27 | def _send_alert_update(self, service, users, duty_officers): 28 | """ 29 | To allow easily monkey patching in hooks for all alerts. 30 | e.g. mocking send_alert_update for all plugins in testing 31 | """ 32 | return self.send_alert_update(service, users, duty_officers) 33 | 34 | def send_alert(self, service, users, duty_officers): 35 | """ 36 | Implement a send_alert function here that shall be called. 37 | """ 38 | return True 39 | 40 | 41 | class AlertPluginUserData(PolymorphicModel): 42 | title = models.CharField(max_length=30, editable=False) 43 | user = models.ForeignKey('UserProfile', editable=False) 44 | 45 | class Meta: 46 | unique_together = ('title', 'user',) 47 | 48 | def __unicode__(self): 49 | return u'%s' % (self.title) 50 | 51 | def serialize(self): 52 | return {} 53 | 54 | 55 | def send_alert(service, duty_officers=None): 56 | users = service.users_to_notify.filter(is_active=True) 57 | for alert in service.alerts.filter(enabled=True): 58 | try: 59 | alert._send_alert(service, users, duty_officers) 60 | except Exception as e: 61 | logging.exception('Could not send %s alert: %s' % (alert.name, e)) 62 | 63 | 64 | def send_alert_update(service, duty_officers=None): 65 | users = service.users_to_notify.filter(is_active=True) 66 | for alert in service.alerts.filter(enabled=True): 67 | if hasattr(alert, 'send_alert_update'): 68 | try: 69 | alert._send_alert_update(service, users, duty_officers) 70 | except Exception as e: 71 | logger.exception('Could not send %s alert update: %s' % (alert.name, e)) 72 | else: 73 | logger.warning('No send_alert_update method present for %s' % alert.name) 74 | 75 | 76 | def update_alert_plugins(): 77 | for plugin_subclass in AlertPlugin.__subclasses__(): 78 | plugin = plugin_subclass.objects.get_or_create(title=plugin_subclass.name) 79 | return AlertPlugin.objects.all() 80 | -------------------------------------------------------------------------------- /cabot/cabotapp/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.db.models.signals import post_migrate 3 | 4 | 5 | def post_migrate_callback(**kwargs): 6 | from cabot.cabotapp.alert import update_alert_plugins 7 | from cabot.cabotapp.models import create_default_jenkins_config 8 | update_alert_plugins() 9 | create_default_jenkins_config() 10 | 11 | class CabotappConfig(AppConfig): 12 | name = 'cabot.cabotapp' 13 | 14 | def ready(self): 15 | post_migrate.connect(post_migrate_callback, sender=self) 16 | -------------------------------------------------------------------------------- /cabot/cabotapp/calendar.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.conf import settings 3 | from django.utils.timezone import now, get_current_timezone 4 | from dateutil import rrule 5 | from icalendar import Calendar 6 | import requests 7 | 8 | import logging 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | MAX_FUTURE = 60 # days 13 | 14 | 15 | def ensure_tzaware(dt): 16 | if dt.tzinfo is None: 17 | return get_current_timezone().localize(dt) 18 | return dt 19 | 20 | 21 | def _recurring_component_to_events(component): 22 | """ 23 | Given an icalendar component with an "RRULE" 24 | Return a list of events as dictionaries 25 | """ 26 | rrule_as_str = component.get('rrule').to_ical() 27 | recur_rule = rrule.rrulestr(rrule_as_str, 28 | dtstart=ensure_tzaware(component.decoded('dtstart'))) 29 | recur_set = rrule.rruleset() 30 | recur_set.rrule(recur_rule) 31 | if 'exdate' in component: 32 | lines = component.decoded('exdate') 33 | if not hasattr(lines, '__iter__'): 34 | lines = [lines] 35 | for exdate_line in lines: 36 | for exdate in exdate_line.dts: 37 | recur_set.exdate(ensure_tzaware(exdate.dt)) 38 | 39 | # get list of events in MAX_FUTURE days 40 | utcnow = now() 41 | later = utcnow + datetime.timedelta(days=MAX_FUTURE) 42 | start_times = recur_set.between(utcnow, later) 43 | 44 | # build list of events 45 | event_length = component.decoded('dtend') - component.decoded('dtstart') 46 | events = [] 47 | for start in start_times: 48 | events.append({ 49 | 'start': start, 50 | 'end': start + event_length, 51 | 'summary': component.decoded('summary'), 52 | 'uid': component.decoded('uid'), 53 | 'last_modified': component.decoded('last-modified'), 54 | }) 55 | return events 56 | 57 | 58 | def get_calendar_data(): 59 | feed_url = settings.CALENDAR_ICAL_URL 60 | resp = requests.get(feed_url) 61 | cal = Calendar.from_ical(resp.content) 62 | return cal 63 | 64 | 65 | def get_events(): 66 | events = [] 67 | for component in get_calendar_data().walk(): 68 | if component.name == 'VEVENT': 69 | if 'rrule' in component: 70 | events.extend(_recurring_component_to_events(component)) 71 | else: 72 | try: 73 | events.append({ 74 | 'start': component.decoded('dtstart'), 75 | 'end': component.decoded('dtend'), 76 | 'summary': component.decoded('summary'), 77 | 'uid': component.decoded('uid'), 78 | 'last_modified': component.decoded('last-modified'), 79 | }) 80 | except KeyError: 81 | logger.debug('Failed to parse VEVENT component: %s', 82 | component.get('uid', 'no uid available')) 83 | return events 84 | -------------------------------------------------------------------------------- /cabot/cabotapp/graphite.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | import requests 3 | import logging 4 | import time 5 | 6 | graphite_api = settings.GRAPHITE_API 7 | user = settings.GRAPHITE_USER 8 | password = settings.GRAPHITE_PASS 9 | graphite_from = settings.GRAPHITE_FROM 10 | auth = (user, password) 11 | 12 | 13 | def get_data(target_pattern, mins_to_check=None): 14 | 15 | if mins_to_check: 16 | _from = '-%dminute' % mins_to_check 17 | else: 18 | _from = graphite_from 19 | 20 | resp = requests.get( 21 | graphite_api + 'render', auth=auth, 22 | params={ 23 | 'target': target_pattern, 24 | 'format': 'json', 25 | 'from': _from, 26 | } 27 | ) 28 | resp.raise_for_status() 29 | return resp.json() 30 | 31 | 32 | def get_matching_metrics(pattern): 33 | print 'Getting metrics matching %s' % pattern 34 | resp = requests.get( 35 | graphite_api + 'metrics/find/', auth=auth, 36 | params={ 37 | 'query': pattern, 38 | 'format': 'completer' 39 | }, 40 | headers={ 41 | 'accept': 'application/json' 42 | } 43 | ) 44 | resp.raise_for_status() 45 | return resp.json() 46 | 47 | 48 | def get_all_metrics(limit=None): 49 | """Grabs all metrics by navigating find API recursively""" 50 | metrics = [] 51 | 52 | def get_leafs_of_node(nodepath): 53 | for obj in get_matching_metrics(nodepath)['metrics']: 54 | if int(obj['is_leaf']) == 1: 55 | metrics.append(obj['path']) 56 | else: 57 | get_leafs_of_node(obj['path']) 58 | get_leafs_of_node('') 59 | return metrics 60 | 61 | 62 | def parse_metric(metric, mins_to_check=5, utcnow=None): 63 | if utcnow is None: 64 | utcnow = time.time() 65 | ret = { 66 | 'num_series_with_data': 0, 67 | 'num_series_no_data': 0, 68 | 'error': None, 69 | 'raw': '', 70 | 'series': [], 71 | } 72 | try: 73 | data = get_data(metric, mins_to_check) 74 | except requests.exceptions.RequestException, e: 75 | ret['error'] = 'Error getting data from Graphite: %s' % e 76 | ret['raw'] = ret['error'] 77 | logging.error('Error getting data from Graphite: %s' % e) 78 | return ret 79 | all_values = [] 80 | for target in data: 81 | series = {'values': [ 82 | float(t[0]) for t in target['datapoints'] if validate_datapoint(t, mins_to_check, utcnow)]} 83 | series["target"] = target["target"] 84 | all_values.extend(series['values']) 85 | if series['values']: 86 | ret['num_series_with_data'] += 1 87 | series['max'] = max(series['values']) 88 | series['min'] = min(series['values']) 89 | series['average_value'] = sum(series['values']) / len(series['values']) 90 | ret['series'].append(series) 91 | else: 92 | ret['num_series_no_data'] += 1 93 | if all_values: 94 | ret['average_value'] = sum(all_values) / len(all_values) 95 | ret['all_values'] = all_values 96 | ret['raw'] = data 97 | return ret 98 | 99 | def validate_datapoint(datapoint, mins_to_check, utcnow): 100 | val, timestamp = datapoint 101 | secs_to_check = 60 * mins_to_check 102 | if val is None: 103 | return False 104 | if timestamp > (utcnow - secs_to_check): 105 | return True 106 | else: 107 | return False 108 | 109 | 110 | -------------------------------------------------------------------------------- /cabot/cabotapp/jenkins.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from datetime import datetime 4 | 5 | import jenkins 6 | from celery.utils.log import get_task_logger 7 | from django.conf import settings 8 | from django.utils import timezone 9 | 10 | logger = get_task_logger(__name__) 11 | 12 | 13 | def _get_jenkins_client(jenkins_config): 14 | return jenkins.Jenkins(jenkins_config.jenkins_api, 15 | username=jenkins_config.jenkins_user, 16 | password=jenkins_config.jenkins_pass) 17 | 18 | def get_job_status(jenkins_config, jobname): 19 | ret = { 20 | 'active': None, 21 | 'succeeded': None, 22 | 'job_number': None, 23 | 'blocked_build_time': None, 24 | } 25 | client = _get_jenkins_client(jenkins_config) 26 | try: 27 | job = client.get_job_info(jobname) 28 | last_completed_build = job['lastCompletedBuild'] 29 | if not last_completed_build: 30 | raise Exception("job has no build") 31 | last_build = client.get_build_info(jobname, last_completed_build['number']) 32 | 33 | if job['lastSuccessfulBuild']: 34 | last_good_build_number = job['lastSuccessfulBuild']['number'] 35 | else: 36 | last_good_build_number = 0 37 | 38 | ret['status_code'] = 200 39 | ret['job_number'] = last_build['number'] 40 | ret['active'] = job['color'] != 'disabled' 41 | ret['succeeded'] = ret['active'] and last_build['result'] == 'SUCCESS' 42 | ret['consecutive_failures'] = last_build['number'] - last_good_build_number 43 | 44 | if job['inQueue']: 45 | in_queued_since = job['queueItem']['inQueueSince'] 46 | time_blocked_since = datetime.utcfromtimestamp( 47 | float(in_queued_since) / 1000).replace(tzinfo=timezone.utc) 48 | ret['blocked_build_time'] = (timezone.now() - time_blocked_since).total_seconds() 49 | ret['queued_job_number'] = job['lastBuild']['number'] 50 | return ret 51 | except jenkins.NotFoundException: 52 | ret['status_code'] = 404 53 | return ret 54 | -------------------------------------------------------------------------------- /cabot/cabotapp/migrations/0002_auto_20170131_1537.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('cabotapp', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RenameField( 15 | model_name='statuscheckresult', 16 | old_name='check', 17 | new_name='status_check', 18 | ), 19 | migrations.AlterIndexTogether( 20 | name='statuscheckresult', 21 | index_together=set([('status_check', 'time_complete'), ('status_check', 'id')]), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /cabot/cabotapp/migrations/0003_auto_20170201_1045.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('cabotapp', '0002_auto_20170131_1537'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='alertplugin', 16 | name='polymorphic_ctype', 17 | field=models.ForeignKey(related_name='polymorphic_cabotapp.alertplugin_set+', editable=False, to='contenttypes.ContentType', null=True), 18 | ), 19 | migrations.AlterField( 20 | model_name='alertpluginuserdata', 21 | name='polymorphic_ctype', 22 | field=models.ForeignKey(related_name='polymorphic_cabotapp.alertpluginuserdata_set+', editable=False, to='contenttypes.ContentType', null=True), 23 | ), 24 | migrations.AlterField( 25 | model_name='statuscheck', 26 | name='polymorphic_ctype', 27 | field=models.ForeignKey(related_name='polymorphic_cabotapp.statuscheck_set+', editable=False, to='contenttypes.ContentType', null=True), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /cabot/cabotapp/migrations/0004_auto_20170802_1327.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('cabotapp', '0003_auto_20170201_1045'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='Service', 16 | name='is_public', 17 | field=models.BooleanField(default=False, help_text=b'The service will be shown in the public home', verbose_name=b'Is Public'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /cabot/cabotapp/migrations/0005_auto_20170818_1202.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-08-18 12:02 3 | from __future__ import unicode_literals 4 | 5 | import os 6 | 7 | import django.db.models.deletion 8 | from django.contrib.contenttypes.models import ContentType 9 | from django.db import migrations, models 10 | 11 | 12 | def move_old_jenkins_checks(apps, schema_editor): 13 | db_alias = schema_editor.connection.alias 14 | 15 | JenkinsStatusCheck = apps.get_model("cabotapp", "JenkinsStatusCheck") 16 | JenkinsCheck = apps.get_model("cabotapp", "JenkinsCheck") 17 | JenkinsConfig = apps.get_model("cabotapp", "JenkinsConfig") 18 | 19 | # Due to a polymorphic bug, JenkinsStatusCheck actually returns all status checks 20 | # Use this to filter out the other checks. 21 | jenkins_content_type = ContentType.objects.filter(model="jenkinsstatuscheck").first() 22 | 23 | if jenkins_content_type and not JenkinsStatusCheck.objects.filter(polymorphic_ctype_id=jenkins_content_type.id).exists(): 24 | return 25 | 26 | if not JenkinsConfig.objects.exists(): 27 | JenkinsConfig.objects.create( 28 | name="Default Jenkins", 29 | jenkins_api=os.environ.get("JENKINS_API", "http://jenkins.example.com"), 30 | jenkins_user=os.environ.get("JENKINS_USER", ""), 31 | jenkins_pass=os.environ.get("JENKINS_PASS", ""), 32 | ) 33 | 34 | default_config = JenkinsConfig.objects.first() 35 | 36 | for old_check in JenkinsStatusCheck.objects.all(): 37 | if old_check.polymorphic_ctype_id != jenkins_content_type.id: 38 | continue 39 | new_check = JenkinsCheck( 40 | active=old_check.active, 41 | allowed_num_failures=old_check.allowed_num_failures, 42 | cached_health=old_check.cached_health, 43 | calculated_status=old_check.calculated_status, 44 | check_type=old_check.check_type, 45 | created_by_id=old_check.created_by_id, 46 | debounce=old_check.debounce, 47 | endpoint=old_check.endpoint, 48 | expected_num_hosts=old_check.expected_num_hosts, 49 | frequency=old_check.frequency, 50 | importance=old_check.importance, 51 | last_run=old_check.last_run, 52 | max_queued_build_time=old_check.max_queued_build_time, 53 | metric=old_check.metric, 54 | name=old_check.name, 55 | password=old_check.password, 56 | status_code=old_check.status_code, 57 | text_match=old_check.text_match, 58 | timeout=old_check.timeout, 59 | username=old_check.username, 60 | value=old_check.value, 61 | jenkins_config=default_config, 62 | # For some reason this isn't handled automatically... 63 | # The model is renamed in the next migration so the ctype 64 | # id stays consistent. 65 | polymorphic_ctype_id=old_check.polymorphic_ctype_id 66 | ) 67 | new_check.save(using=db_alias) 68 | new_check.service_set.add(*old_check.service_set.all()) 69 | new_check.instance_set.add(*old_check.instance_set.all()) 70 | new_check.save(using=db_alias) 71 | old_check.delete(using=db_alias) 72 | 73 | 74 | class Migration(migrations.Migration): 75 | 76 | dependencies = [ 77 | ('cabotapp', '0004_auto_20170802_1327'), 78 | ] 79 | 80 | operations = [ 81 | migrations.CreateModel( 82 | name='JenkinsCheck', 83 | fields=[ 84 | ('statuscheck_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cabotapp.StatusCheck')), 85 | ], 86 | options={ 87 | 'abstract': False, 88 | }, 89 | bases=('cabotapp.statuscheck',), 90 | ), 91 | migrations.CreateModel( 92 | name='JenkinsConfig', 93 | fields=[ 94 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 95 | ('name', models.CharField(max_length=30)), 96 | ('jenkins_api', models.CharField(max_length=2000)), 97 | ('jenkins_user', models.CharField(max_length=2000)), 98 | ('jenkins_pass', models.CharField(max_length=2000)), 99 | ], 100 | ), 101 | migrations.AddField( 102 | model_name='jenkinscheck', 103 | name='jenkins_config', 104 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cabotapp.JenkinsConfig'), 105 | ), 106 | migrations.RunPython(move_old_jenkins_checks) 107 | ] 108 | -------------------------------------------------------------------------------- /cabot/cabotapp/migrations/0006_auto_20170821_1000.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-08-21 10:00 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('cabotapp', '0005_auto_20170818_1202'), 12 | ] 13 | 14 | operations = [ 15 | migrations.DeleteModel( 16 | name='JenkinsStatusCheck', 17 | ), 18 | migrations.RenameModel( 19 | old_name='JenkinsCheck', 20 | new_name='JenkinsStatusCheck', 21 | ) 22 | ] 23 | -------------------------------------------------------------------------------- /cabot/cabotapp/migrations/0007_statuscheckresult_consecutive_failures.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-08-24 11:19 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('cabotapp', '0006_auto_20170821_1000'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='statuscheckresult', 17 | name='consecutive_failures', 18 | field=models.PositiveIntegerField(null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /cabot/cabotapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/cabotapp/migrations/__init__.py -------------------------------------------------------------------------------- /cabot/cabotapp/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | from .jenkins_check_plugin import * 3 | -------------------------------------------------------------------------------- /cabot/cabotapp/models/jenkins_check_plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.db import models 4 | 5 | from ..jenkins import get_job_status 6 | from .base import StatusCheck, StatusCheckResult 7 | 8 | 9 | class JenkinsStatusCheck(StatusCheck): 10 | jenkins_config = models.ForeignKey('JenkinsConfig') 11 | 12 | @property 13 | def check_category(self): 14 | return "Jenkins check" 15 | 16 | @property 17 | def failing_short_status(self): 18 | return 'Job failing on Jenkins' 19 | 20 | def _run(self): 21 | result = StatusCheckResult(status_check=self) 22 | try: 23 | status = get_job_status(self.jenkins_config, self.name) 24 | active = status['active'] 25 | result.job_number = status['job_number'] 26 | result.consecutive_failures = status['consecutive_failures'] 27 | if status['status_code'] == 404: 28 | result.error = u'Job %s not found on Jenkins' % self.name 29 | result.succeeded = False 30 | return result 31 | elif status['status_code'] > 400: 32 | # Will fall through to next block 33 | raise Exception(u'returned %s' % status['status_code']) 34 | except Exception as e: 35 | # If something else goes wrong, we will *not* fail - otherwise 36 | # a lot of services seem to fail all at once. 37 | # Ugly to do it here but... 38 | result.error = u'Error fetching from Jenkins - %s' % e.message 39 | result.succeeded = True 40 | return result 41 | 42 | if not active: 43 | # We will fail if the job has been disabled 44 | result.error = u'Job "%s" disabled on Jenkins' % self.name 45 | result.succeeded = False 46 | else: 47 | if self.max_queued_build_time and status['blocked_build_time']: 48 | if status['blocked_build_time'] > self.max_queued_build_time * 60: 49 | result.succeeded = False 50 | result.error = u'Job "%s" has blocked build waiting for %ss (> %sm)' % ( 51 | self.name, 52 | int(status['blocked_build_time']), 53 | self.max_queued_build_time, 54 | ) 55 | result.job_number = status['queued_job_number'] 56 | else: 57 | result.succeeded = status['succeeded'] 58 | else: 59 | result.succeeded = status['succeeded'] 60 | if not status['succeeded']: 61 | message = u'Job "%s" failing on Jenkins (%s)' % (self.name, status['consecutive_failures']) 62 | if result.error: 63 | result.error += u'; %s' % message 64 | else: 65 | result.error = message 66 | result.raw_data = status 67 | return result 68 | 69 | def calculate_debounced_passing(self, recent_results, debounce=0): 70 | """ 71 | `debounce` is the number of previous job failures we need (not including this) 72 | to mark a search as passing or failing 73 | Returns: 74 | True if passing given debounce factor 75 | False if failing 76 | """ 77 | last_result = recent_results[0] 78 | return last_result.consecutive_failures <= debounce 79 | 80 | 81 | class JenkinsConfig(models.Model): 82 | name = models.CharField(max_length=30, blank=False) 83 | jenkins_api = models.CharField(max_length=2000, blank=False) 84 | jenkins_user = models.CharField(max_length=2000, blank=False) 85 | jenkins_pass = models.CharField(max_length=2000, blank=False) 86 | 87 | def __str__(self): 88 | return self.name 89 | 90 | 91 | def create_default_jenkins_config(): 92 | if not JenkinsConfig.objects.exists(): 93 | if os.environ.get("JENKINS_API"): 94 | JenkinsConfig.objects.create( 95 | name="Default Jenkins", 96 | jenkins_api=os.environ.get("JENKINS_API", "http://jenkins.example.com"), 97 | jenkins_user=os.environ.get("JENKINS_USER", ""), 98 | jenkins_pass=os.environ.get("JENKINS_PASS", ""), 99 | ) 100 | -------------------------------------------------------------------------------- /cabot/cabotapp/tasks.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | 4 | from celery.task import task 5 | from django.conf import settings 6 | from django.utils import timezone 7 | from datetime import timedelta 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | @task(ignore_result=True) 13 | def run_status_check(check_or_id): 14 | from .models import StatusCheck 15 | if not isinstance(check_or_id, StatusCheck): 16 | check = StatusCheck.objects.get(id=check_or_id) 17 | else: 18 | check = check_or_id 19 | # This will call the subclass method 20 | check.run() 21 | 22 | 23 | @task(ignore_result=True) 24 | def run_all_checks(): 25 | from .models import StatusCheck 26 | from datetime import timedelta 27 | checks = StatusCheck.objects.all() 28 | seconds = range(60) 29 | for check in checks: 30 | if check.last_run: 31 | next_schedule = check.last_run + timedelta(minutes=check.frequency) 32 | if (not check.last_run) or timezone.now() > next_schedule: 33 | delay = random.choice(seconds) 34 | logger.debug('Scheduling task for %s seconds from now' % delay) 35 | run_status_check.apply_async((check.id,), countdown=delay) 36 | 37 | 38 | @task(ignore_result=True) 39 | def update_services(ignore_result=True): 40 | # Avoid importerrors and the like from legacy scheduling 41 | return 42 | 43 | 44 | @task(ignore_result=True) 45 | def update_service(service_or_id): 46 | from .models import Service 47 | if not isinstance(service_or_id, Service): 48 | service = Service.objects.get(id=service_or_id) 49 | else: 50 | service = service_or_id 51 | service.update_status() 52 | 53 | 54 | @task(ignore_result=True) 55 | def update_instance(instance_or_id): 56 | from .models import Instance 57 | if not isinstance(instance_or_id, Instance): 58 | instance = Instance.objects.get(id=instance_or_id) 59 | else: 60 | instance = instance_or_id 61 | instance.update_status() 62 | 63 | 64 | @task(ignore_result=True) 65 | def update_shifts(): 66 | from .models import update_shifts as _update_shifts 67 | _update_shifts() 68 | 69 | 70 | @task(ignore_result=True) 71 | def clean_db(days_to_retain=7, batch_size=10000): 72 | """ 73 | Clean up database otherwise it gets overwhelmed with StatusCheckResults. 74 | 75 | To loop over undeleted results, spawn new tasks to make sure db connection closed etc 76 | """ 77 | from .models import StatusCheckResult, ServiceStatusSnapshot, InstanceStatusSnapshot 78 | 79 | to_discard_results = StatusCheckResult.objects.order_by('time_complete').filter( 80 | time_complete__lte=timezone.now() - timedelta(days=days_to_retain) 81 | ) 82 | to_discard_service = ServiceStatusSnapshot.objects.order_by('time').filter( 83 | time__lte=timezone.now() - timedelta(days=days_to_retain) 84 | ) 85 | to_discard_instance = InstanceStatusSnapshot.objects.order_by('time').filter( 86 | time__lte=timezone.now() - timedelta(days=days_to_retain) 87 | ) 88 | 89 | result_ids = to_discard_results[:batch_size].values_list('id', flat=True) 90 | service_snapshot_ids = to_discard_service[:batch_size].values_list('id', flat=True) 91 | instance_snapshot_ids = to_discard_instance[:batch_size].values_list('id', flat=True) 92 | 93 | result_count = result_ids.count() 94 | service_snapshot_count = service_snapshot_ids.count() 95 | instance_snapshot_count = instance_snapshot_ids.count() 96 | 97 | StatusCheckResult.objects.filter(id__in=result_ids).delete() 98 | ServiceStatusSnapshot.objects.filter(id__in=service_snapshot_ids).delete() 99 | InstanceStatusSnapshot.objects.filter(id__in=instance_snapshot_ids).delete() 100 | 101 | # If we reached the batch size on either we need to re-queue to continue cleaning up. 102 | if ( 103 | result_count == batch_size or service_snapshot_count == batch_size or instance_snapshot_count == batch_size 104 | ): 105 | clean_db.apply_async(kwargs={ 106 | 'days_to_retain': days_to_retain, 107 | 'batch_size': batch_size}, 108 | countdown=3) 109 | -------------------------------------------------------------------------------- /cabot/cabotapp/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/cabotapp/templatetags/__init__.py -------------------------------------------------------------------------------- /cabot/cabotapp/templatetags/extra.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | from datetime import timedelta 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.simple_tag 9 | def jenkins_human_url(jobname): 10 | return '{}job/{}/'.format(settings.JENKINS_API, jobname) 11 | 12 | 13 | @register.simple_tag 14 | def echo_setting(setting): 15 | return getattr(settings, setting, '') 16 | 17 | 18 | @register.filter(name='format_timedelta') 19 | def format_timedelta(delta): 20 | # Getting rid of microseconds. 21 | return str(timedelta(days=delta.days, seconds=delta.seconds)) 22 | 23 | @register.filter 24 | def for_service(objects, service): 25 | return objects.filter(service=service) 26 | -------------------------------------------------------------------------------- /cabot/cabotapp/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/cabotapp/tests/__init__.py -------------------------------------------------------------------------------- /cabot/cabotapp/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/cabotapp/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /cabot/cabotapp/tests/fixtures/cabot_check_skeleton/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/cabotapp/tests/fixtures/cabot_check_skeleton/__init__.py -------------------------------------------------------------------------------- /cabot/cabotapp/tests/fixtures/cabot_check_skeleton/plugin.py: -------------------------------------------------------------------------------- 1 | from cabot.cabotapp.models import StatusCheck 2 | from cabot.cabotapp.views import CheckCreateView 3 | from cabot.cabotapp.views import CheckUpdateView 4 | from cabot.cabotapp.views import StatusCheckForm 5 | from django.http import HttpResponseRedirect 6 | from django.core.urlresolvers import reverse 7 | 8 | class SkeletonStatusCheck(StatusCheck): 9 | edit_url_name = 'update-skeleton-check' 10 | duplicate_url_name = 'duplicate-skeleton-check' 11 | 12 | check_name = 'skeleton' 13 | 14 | class SkeletonStatusCheckForm(StatusCheckForm): 15 | class Meta: 16 | model = SkeletonStatusCheck 17 | fields = ('name',) 18 | 19 | class SkeletonCheckCreateView(CheckCreateView): 20 | model = StatusCheck 21 | form_class = SkeletonStatusCheckForm 22 | 23 | class SkeletonCheckUpdateView(CheckUpdateView): 24 | model = StatusCheck 25 | form_class = SkeletonStatusCheckForm 26 | 27 | def duplicate_check(request, pk): 28 | return HttpResponseRedirect(reverse('update-skeleton-check', kwargs={'pk': 25})) 29 | -------------------------------------------------------------------------------- /cabot/cabotapp/tests/fixtures/gcal_response.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | METHOD:PUBLISH 6 | X-WR-CALNAME:Cabottest 7 | X-WR-TIMEZONE:Europe/Copenhagen 8 | X-WR-CALDESC: 9 | BEGIN:VEVENT 10 | DTSTART:20160719T130000Z 11 | DTEND:20160719T160000Z 12 | DTSTAMP:20160719T102406Z 13 | UID:kf6gd4uc1hue70m7gkb7fdtsrc@google.com 14 | CREATED:20160719T091318Z 15 | DESCRIPTION: 16 | LAST-MODIFIED:20160719T091320Z 17 | LOCATION: 18 | SEQUENCE:0 19 | STATUS:CONFIRMED 20 | SUMMARY:troels 21 | TRANSP:OPAQUE 22 | END:VEVENT 23 | END:VCALENDAR -------------------------------------------------------------------------------- /cabot/cabotapp/tests/fixtures/graphite_avg_response.json: -------------------------------------------------------------------------------- 1 | [{"target": "PROD", "datapoints": [[20.3, 1442834740], [12.5, 1442834750], [0.1, 1442834760], [0.8, 1442834770], [134.9, 1442834780], [151.0, 1442834790]]}] -------------------------------------------------------------------------------- /cabot/cabotapp/tests/fixtures/graphite_null_response.json: -------------------------------------------------------------------------------- 1 | [{"target": "minSeries(hosts.1.df.var.df_complex.free.value,hosts.2.df.var.df_complex.free.value,hosts.3.df.var.df_complex.free.value,hosts.4.df.var.df_complex.free.value)", "datapoints": [[null, 1441144030], [null, 1441144040], [null, 1441144050], [null, 1441144060], [null, 1441144070], [null, 1441144080], [null, 1441144090], [null, 1441144100], [null, 1441144110], [null, 1441144120], [null, 1441144130], [null, 1441144140], [null, 1441144150], [null, 1441144160], [null, 1441144170], [null, 1441144180], [null, 1441144190], [null, 1441144200], [null, 1441144210], [null, 1441144220], [null, 1441144230], [null, 1441144240], [null, 1441144250], [null, 1441144260], [null, 1441144270], [null, 1441144280], [null, 1441144290], [null, 1441144300], [null, 1441144310], [null, 1441144320], [null, 1441144330], [null, 1441144340], [null, 1441144350], [null, 1441144360], [null, 1441144370], [null, 1441144380], [null, 1441144390], [null, 1441144400], [null, 1441144410], [null, 1441144420], [null, 1441144430], [null, 1441144440], [null, 1441144450], [null, 1441144460], [null, 1441144470], [null, 1441144480], [null, 1441144490], [null, 1441144500], [null, 1441144510], [null, 1441144520], [null, 1441144530], [null, 1441144540], [null, 1441144550], [null, 1441144560], [null, 1441144570], [null, 1441144580], [null, 1441144590], [null, 1441144600], [null, 1441144610], [null, 1441144620]]}] -------------------------------------------------------------------------------- /cabot/cabotapp/tests/fixtures/graphite_response.json: -------------------------------------------------------------------------------- 1 | [{"target": "PROD", "datapoints": [[8.14908, 1387817760], [8.14908, 1387817820], [8.14908, 1387817880], [8.14908, 1387817940], [8.14908, 1387818000], [8.14908, 1387818060], [8.14908, 1387818120], [8.14908, 1387818180], [8.14908, 1387818240], [8.14908, 1387818300], [9.16092, 1387818360], [9.16092, 1387818420], [9.16092, 1387818480], [9.16092, 1387818540], [9.16092, 1387818600]]}, {"target": "stage", "datapoints": [[8.17349, 1387817760], [8.17349, 1387817820], [8.17349, 1387817880], [8.17349, 1387817940], [8.17349, 1387818000], [8.16242, 1387818060], [8.16242, 1387818120], [8.16242, 1387818180], [8.16242, 1387818240], [8.16242, 1387818300], [8.16242, 1387818360], [8.16242, 1387818420], [8.16242, 1387818480], [8.16242, 1387818540], [8.16242, 1387818600]]}] -------------------------------------------------------------------------------- /cabot/cabotapp/tests/fixtures/recurring_response.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | METHOD:PUBLISH 6 | X-WR-CALNAME:Cabot Rota 7 | X-WR-TIMEZONE:America/New_York 8 | X-WR-CALDESC: 9 | BEGIN:VTIMEZONE 10 | TZID:America/New_York 11 | X-LIC-LOCATION:America/New_York 12 | BEGIN:DAYLIGHT 13 | TZOFFSETFROM:-0500 14 | TZOFFSETTO:-0400 15 | TZNAME:EDT 16 | DTSTART:19700308T020000 17 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 18 | END:DAYLIGHT 19 | BEGIN:STANDARD 20 | TZOFFSETFROM:-0400 21 | TZOFFSETTO:-0500 22 | TZNAME:EST 23 | DTSTART:19701101T020000 24 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 25 | END:STANDARD 26 | END:VTIMEZONE 27 | BEGIN:VEVENT 28 | DTSTART;TZID=America/New_York:20161129T080000 29 | DTEND;TZID=America/New_York:20161130T080000 30 | RRULE:FREQ=DAILY;INTERVAL=2 31 | DTSTAMP:20161128T214445Z 32 | UID:mep8lmf2s1lhmmm366c6rhhers@google.com 33 | CREATED:20161128T204255Z 34 | DESCRIPTION: 35 | LAST-MODIFIED:20161128T204255Z 36 | LOCATION: 37 | SEQUENCE:0 38 | STATUS:CONFIRMED 39 | SUMMARY:foo 40 | TRANSP:OPAQUE 41 | END:VEVENT 42 | BEGIN:VEVENT 43 | DTSTART;TZID=America/New_York:20161128T080000 44 | DTEND;TZID=America/New_York:20161129T080000 45 | RRULE:FREQ=DAILY;INTERVAL=2 46 | DTSTAMP:20161128T214445Z 47 | UID:s4ftq5jd98cuubmbous3mgst5c@google.com 48 | CREATED:20161128T203907Z 49 | DESCRIPTION: 50 | LAST-MODIFIED:20161128T203947Z 51 | LOCATION: 52 | SEQUENCE:1 53 | STATUS:CONFIRMED 54 | SUMMARY:bar 55 | TRANSP:OPAQUE 56 | END:VEVENT 57 | END:VCALENDAR 58 | -------------------------------------------------------------------------------- /cabot/cabotapp/tests/fixtures/recurring_response_complex.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | METHOD:PUBLISH 6 | X-WR-CALNAME:Cabot Rota 7 | X-WR-TIMEZONE:America/New_York 8 | X-WR-CALDESC: 9 | BEGIN:VTIMEZONE 10 | TZID:America/New_York 11 | X-LIC-LOCATION:America/New_York 12 | BEGIN:DAYLIGHT 13 | TZOFFSETFROM:-0500 14 | TZOFFSETTO:-0400 15 | TZNAME:EDT 16 | DTSTART:19700308T020000 17 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 18 | END:DAYLIGHT 19 | BEGIN:STANDARD 20 | TZOFFSETFROM:-0400 21 | TZOFFSETTO:-0500 22 | TZNAME:EST 23 | DTSTART:19701101T020000 24 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 25 | END:STANDARD 26 | END:VTIMEZONE 27 | BEGIN:VEVENT 28 | DTSTART;TZID=America/New_York:20170410T080000 29 | DTEND;TZID=America/New_York:20170411T080000 30 | DTSTAMP:20170426T030852Z 31 | UID:s4ftq5jd98cuubmbous3mgst5c@google.com 32 | RECURRENCE-ID;TZID=America/New_York:20170411T080000 33 | CREATED:20161128T203907Z 34 | DESCRIPTION: 35 | LAST-MODIFIED:20170409T202640Z 36 | LOCATION: 37 | SEQUENCE:2 38 | STATUS:CONFIRMED 39 | SUMMARY:foo 40 | TRANSP:OPAQUE 41 | END:VEVENT 42 | BEGIN:VEVENT 43 | DTSTART;TZID=America/New_York:20170409T080000 44 | DTEND;TZID=America/New_York:20170410T080000 45 | DTSTAMP:20170426T030852Z 46 | UID:mep8lmf2s1lhmmm366c6rhhers@google.com 47 | RECURRENCE-ID;TZID=America/New_York:20170410T080000 48 | CREATED:20161128T204255Z 49 | DESCRIPTION: 50 | LAST-MODIFIED:20170409T202639Z 51 | LOCATION: 52 | SEQUENCE:1 53 | STATUS:CONFIRMED 54 | SUMMARY:bar 55 | TRANSP:OPAQUE 56 | END:VEVENT 57 | BEGIN:VEVENT 58 | DTSTART;TZID=America/New_York:20170411T080000 59 | DTEND;TZID=America/New_York:20170412T080000 60 | DTSTAMP:20170426T030852Z 61 | UID:s4ftq5jd98cuubmbous3mgst5c@google.com 62 | RECURRENCE-ID;TZID=America/New_York:20170409T080000 63 | CREATED:20161128T203907Z 64 | DESCRIPTION: 65 | LAST-MODIFIED:20170409T202637Z 66 | LOCATION: 67 | SEQUENCE:2 68 | STATUS:CONFIRMED 69 | SUMMARY:melissa 70 | TRANSP:OPAQUE 71 | END:VEVENT 72 | BEGIN:VEVENT 73 | DTSTART;TZID=America/New_York:20170219T080000 74 | DTEND;TZID=America/New_York:20170220T080000 75 | DTSTAMP:20170426T030852Z 76 | UID:s4ftq5jd98cuubmbous3mgst5c@google.com 77 | RECURRENCE-ID;TZID=America/New_York:20170220T080000 78 | CREATED:20161128T203907Z 79 | DESCRIPTION: 80 | LAST-MODIFIED:20170208T003612Z 81 | LOCATION: 82 | SEQUENCE:2 83 | STATUS:CONFIRMED 84 | SUMMARY:foo 85 | TRANSP:OPAQUE 86 | END:VEVENT 87 | BEGIN:VEVENT 88 | DTSTART;TZID=America/New_York:20170217T080000 89 | DTEND;TZID=America/New_York:20170219T080000 90 | DTSTAMP:20170426T030852Z 91 | UID:mep8lmf2s1lhmmm366c6rhhers@google.com 92 | RECURRENCE-ID;TZID=America/New_York:20170217T080000 93 | CREATED:20161128T204255Z 94 | DESCRIPTION: 95 | LAST-MODIFIED:20170208T003542Z 96 | LOCATION: 97 | SEQUENCE:0 98 | STATUS:CONFIRMED 99 | SUMMARY:bar 100 | TRANSP:OPAQUE 101 | END:VEVENT 102 | BEGIN:VEVENT 103 | DTSTART;TZID=America/New_York:20161128T080000 104 | DTEND;TZID=America/New_York:20161129T080000 105 | RRULE:FREQ=DAILY;INTERVAL=2 106 | EXDATE;TZID=America/New_York:20170218T080000 107 | DTSTAMP:20170426T030852Z 108 | UID:s4ftq5jd98cuubmbous3mgst5c@google.com 109 | CREATED:20161128T203907Z 110 | DESCRIPTION: 111 | LAST-MODIFIED:20161130T013836Z 112 | LOCATION: 113 | SEQUENCE:1 114 | STATUS:CONFIRMED 115 | SUMMARY:foo 116 | TRANSP:OPAQUE 117 | END:VEVENT 118 | BEGIN:VEVENT 119 | DTSTART;TZID=America/New_York:20161129T080000 120 | DTEND;TZID=America/New_York:20161130T080000 121 | RRULE:FREQ=DAILY;INTERVAL=2 122 | EXDATE;TZID=America/New_York:20170219T080000 123 | DTSTAMP:20170426T030852Z 124 | UID:mep8lmf2s1lhmmm366c6rhhers@google.com 125 | CREATED:20161128T204255Z 126 | DESCRIPTION: 127 | LAST-MODIFIED:20161128T204255Z 128 | LOCATION: 129 | SEQUENCE:0 130 | STATUS:CONFIRMED 131 | SUMMARY:gregory 132 | TRANSP:OPAQUE 133 | END:VEVENT 134 | END:VCALENDAR 135 | -------------------------------------------------------------------------------- /cabot/cabotapp/tests/fixtures/recurring_response_notz.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | METHOD:PUBLISH 6 | X-WR-CALNAME:Cabot Rota 7 | X-WR-CALDESC: 8 | BEGIN:VTIMEZONE 9 | X-LIC-LOCATION:America/New_York 10 | BEGIN:DAYLIGHT 11 | TZOFFSETFROM:-0500 12 | TZOFFSETTO:-0400 13 | TZNAME:EDT 14 | DTSTART:19700308T020000 15 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 16 | END:DAYLIGHT 17 | BEGIN:STANDARD 18 | TZOFFSETFROM:-0400 19 | TZOFFSETTO:-0500 20 | TZNAME:EST 21 | DTSTART:19701101T020000 22 | RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 23 | END:STANDARD 24 | END:VTIMEZONE 25 | BEGIN:VEVENT 26 | DTSTART:20161129T080000 27 | DTEND:20161130T080000 28 | RRULE:FREQ=DAILY;INTERVAL=2 29 | DTSTAMP:20161128T214445Z 30 | UID:mep8lmf2s1lhmmm366c6rhhers@google.com 31 | CREATED:20161128T204255Z 32 | DESCRIPTION: 33 | LAST-MODIFIED:20161128T204255Z 34 | LOCATION: 35 | SEQUENCE:0 36 | STATUS:CONFIRMED 37 | SUMMARY:foo 38 | TRANSP:OPAQUE 39 | END:VEVENT 40 | BEGIN:VEVENT 41 | DTSTART:20161128T080000 42 | DTEND:20161129T080000 43 | RRULE:FREQ=DAILY;INTERVAL=2 44 | DTSTAMP:20161128T214445Z 45 | UID:s4ftq5jd98cuubmbous3mgst5c@google.com 46 | CREATED:20161128T203907Z 47 | DESCRIPTION: 48 | LAST-MODIFIED:20161128T203947Z 49 | LOCATION: 50 | SEQUENCE:1 51 | STATUS:CONFIRMED 52 | SUMMARY:bar 53 | TRANSP:OPAQUE 54 | END:VEVENT 55 | END:VCALENDAR 56 | -------------------------------------------------------------------------------- /cabot/cabotapp/tests/test_plugin_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.contrib.auth import get_user_model 3 | from django.shortcuts import resolve_url 4 | from django.test import TestCase 5 | 6 | from mock import patch 7 | 8 | from cabot.cabotapp.alert import AlertPlugin 9 | from cabot.cabotapp.models import Service 10 | 11 | 12 | class PluginSettingsTest(TestCase): 13 | def setUp(self): 14 | self.username = 'testuser' 15 | self.password = 'testuserpassword' 16 | self.user = get_user_model().objects.create(username=self.username) 17 | self.user.set_password(self.password) 18 | self.user.save() 19 | self.client.login(username=self.username, password=self.password) 20 | 21 | def test_global_settings(self): 22 | resp = self.client.get(resolve_url('plugin-settings-global'), follow=True) 23 | self.assertEqual(resp.status_code, 200) 24 | 25 | def test_plugin_settings(self): 26 | plugin = AlertPlugin.objects.first() 27 | 28 | resp = self.client.get(resolve_url('plugin-settings', plugin_name=plugin.title), follow=True) 29 | self.assertEqual(resp.status_code, 200) 30 | 31 | def test_plugin_disable(self): 32 | plugin = AlertPlugin.objects.first() 33 | 34 | resp = self.client.post(resolve_url('plugin-settings', plugin_name=plugin.title), {'enabled': False}, follow=True) 35 | self.assertEqual(resp.status_code, 200) 36 | self.assertIn('Updated Successfully', resp.content) 37 | 38 | @patch('cabot.cabotapp.alert.AlertPlugin._send_alert') 39 | def test_plugin_alert_test(self, fake_send_alert): 40 | plugin = AlertPlugin.objects.first() 41 | 42 | resp = self.client.post(resolve_url('alert-test-plugin'), {'alert_plugin': plugin.id, 'old_status': 'PASSING', 'new_status': 'ERROR'}) 43 | self.assertEqual(resp.status_code, 200) 44 | self.assertIn('ok', resp.content) 45 | fake_send_alert.assert_called() 46 | 47 | @patch('cabot.cabotapp.alert.AlertPlugin._send_alert') 48 | def test_global_alert_test(self, fake_send_alert): 49 | service = Service.objects.create( 50 | name='Service', 51 | ) 52 | 53 | plugin = AlertPlugin.objects.first() 54 | service.alerts.add( 55 | plugin 56 | ) 57 | 58 | resp = self.client.post(resolve_url('alert-test'), {'service': service.id, 'old_status': 'PASSING', 'new_status': 'ERROR'}) 59 | self.assertEqual(resp.status_code, 200) 60 | self.assertIn('ok', resp.content) 61 | fake_send_alert.assert_called() 62 | -------------------------------------------------------------------------------- /cabot/cabotapp/tests/test_setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from urlparse import urlparse 3 | 4 | from django.contrib.auth import get_user_model 5 | from django.shortcuts import resolve_url 6 | from django.test import TestCase 7 | 8 | 9 | class SetupTest(TestCase): 10 | def test_initial_setup_redirect(self): 11 | resp = self.client.get(resolve_url('login')) 12 | 13 | self.assertEqual(resp.status_code, 302) 14 | 15 | url = urlparse(resp['Location']) 16 | self.assertEqual(url.path, resolve_url('first_time_setup')) 17 | 18 | # Don't redirect if there's already a user 19 | get_user_model().objects.create_user(username='test') 20 | resp = self.client.get(resolve_url('login')) 21 | self.assertEqual(resp.status_code, 200) 22 | 23 | def test_initial_setup_requires(self): 24 | resp = self.client.post(resolve_url('first_time_setup')) 25 | self.assertEqual(resp.status_code, 400) 26 | 27 | def test_initial_setup_post(self): 28 | resp = self.client.post( 29 | resolve_url('first_time_setup'), 30 | data={ 31 | 'username': '', 32 | 'password': 'pass' 33 | }) 34 | self.assertEqual(resp.status_code, 400) 35 | 36 | resp = self.client.post( 37 | resolve_url('first_time_setup'), 38 | data={ 39 | 'username': 'test', 40 | 'password': '' 41 | }) 42 | self.assertEqual(resp.status_code, 400) 43 | self.assertFalse(get_user_model().objects.exists()) 44 | 45 | resp = self.client.post( 46 | resolve_url('first_time_setup'), 47 | data={ 48 | 'username': 'test', 49 | 'password': 'pass' 50 | }) 51 | self.assertEqual(resp.status_code, 302) 52 | self.assertTrue(get_user_model().objects.exists()) 53 | 54 | def test_initial_setup_post_with_email(self): 55 | resp = self.client.post( 56 | resolve_url('first_time_setup'), 57 | data={ 58 | 'username': 'test', 59 | 'email': 'fail', 60 | 'password': 'pass' 61 | }) 62 | self.assertEqual(resp.status_code, 400) 63 | self.assertFalse(get_user_model().objects.exists()) 64 | 65 | resp = self.client.post( 66 | resolve_url('first_time_setup'), 67 | data={ 68 | 'username': 'test', 69 | 'email': 'real@email.com', 70 | 'password': 'pass' 71 | }) 72 | self.assertEqual(resp.status_code, 302) 73 | self.assertTrue(get_user_model().objects.exists()) 74 | 75 | def test_cant_setup_with_existing_user(self): 76 | get_user_model().objects.create_user(username='test') 77 | 78 | resp = self.client.post( 79 | resolve_url('first_time_setup'), 80 | data={ 81 | 'username': 'test', 82 | 'email': 'real@email.com', 83 | 'password': 'pass' 84 | }) 85 | self.assertEqual(get_user_model().objects.count(), 1) 86 | 87 | -------------------------------------------------------------------------------- /cabot/cabotapp/tests/test_urlprefix.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.core.urlresolvers import reverse, clear_url_caches 4 | from django.conf import settings 5 | from django.test.utils import override_settings 6 | from importlib import import_module 7 | 8 | from rest_framework import status, HTTP_HEADER_ENCODING 9 | 10 | from tests_basic import LocalTestCase 11 | 12 | class override_local_settings(override_settings): 13 | def clear_cache(self): 14 | # If we don't do this, nothing gets correctly set for the URL Prefix 15 | urlconf = settings.ROOT_URLCONF 16 | if urlconf in sys.modules: 17 | reload(sys.modules[urlconf]) 18 | import_module(urlconf) 19 | 20 | # Don't forget to clear out the cache for `reverse` 21 | clear_url_caches() 22 | 23 | def __init__(self, urlprefix, custom_check_plugins): 24 | urlprefix = urlprefix.rstrip("/") 25 | installed_apps = settings.INSTALLED_APPS 26 | installed_apps += tuple(custom_check_plugins) 27 | 28 | # Have to turn off the compressor here, can't find a way to reload 29 | # the COMPRESS_URL into it on the fly 30 | super(override_local_settings, self).__init__( 31 | URL_PREFIX=urlprefix, 32 | MEDIA_URL="%s/media/" % urlprefix, 33 | STATIC_URL="%s/static/" % urlprefix, 34 | COMPRESS_URL="%s/static/" % urlprefix, 35 | COMPRESS_ENABLED=False, 36 | COMPRESS_PRECOMPILERS=(), 37 | INSTALLED_APPS=installed_apps 38 | ) 39 | 40 | def __enter__(self): 41 | super(override_local_settings, self).__enter__() 42 | self.clear_cache() 43 | 44 | def __exit__(self, exc_type, exc_value, traceback): 45 | super(override_local_settings, self).__exit__(exc_type, exc_value, traceback) 46 | self.clear_cache() 47 | 48 | def set_url_prefix_and_custom_check_plugins(prefix, plugins): 49 | return override_local_settings(prefix, plugins) 50 | 51 | class URLPrefixTestCase(LocalTestCase): 52 | def set_url_prefix(self, prefix): 53 | return override_local_settings(prefix, []) 54 | 55 | def test_reverse(self): 56 | prefix = '/test' 57 | before = reverse('services') 58 | 59 | with self.set_url_prefix(prefix): 60 | self.assertNotEqual(reverse('services'), before) 61 | self.assertTrue(reverse('services').startswith(prefix)) 62 | self.assertEqual(reverse('services')[len(prefix):], before) 63 | 64 | def test_loginurl(self): 65 | prefix = '/test' 66 | 67 | with self.set_url_prefix(prefix): 68 | loginurl = str(settings.LOGIN_URL) 69 | response = self.client.get(reverse('services')) 70 | 71 | self.assertTrue(loginurl.startswith(prefix)) 72 | self.assertTrue(loginurl in response.url) 73 | 74 | def test_query(self): 75 | prefix = '/test' 76 | self.client.login(username=self.username, password=self.password) 77 | 78 | before_services = self.client.get(reverse('services')) 79 | before_systemstatus = self.client.get(reverse('system-status')) 80 | 81 | with self.set_url_prefix(prefix): 82 | response = self.client.get(reverse('services')) 83 | 84 | self.assertEqual(response.status_code, before_services.status_code) 85 | self.assertNotEqual(response.content, before_services.content) 86 | 87 | self.assertIn(reverse('services'), response.content) 88 | 89 | response_systemstatus = self.client.get(reverse('system-status')) 90 | 91 | self.assertEqual(response_systemstatus.status_code, before_systemstatus.status_code) 92 | -------------------------------------------------------------------------------- /cabot/cabotapp/tests/tests_icmp_check.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from mock import patch 4 | 5 | from cabot.cabotapp.models import ( 6 | ICMPStatusCheck, 7 | Instance, 8 | Service, 9 | ) 10 | 11 | from .tests_basic import LocalTestCase 12 | 13 | class TestICMPCheckRun(LocalTestCase): 14 | 15 | def setUp(self): 16 | super(TestICMPCheckRun, self).setUp() 17 | self.instance = Instance.objects.create( 18 | name='Instance', 19 | address='1.2.3.4' 20 | ) 21 | self.icmp_check = ICMPStatusCheck.objects.create( 22 | name='ICMP Check', 23 | created_by=self.user, 24 | importance=Service.CRITICAL_STATUS, 25 | ) 26 | self.instance.status_checks.add( 27 | self.icmp_check) 28 | 29 | self.patch = patch('cabot.cabotapp.models.subprocess.check_output', autospec=True) 30 | self.mock_check_output = self.patch.start() 31 | 32 | def tearDown(self): 33 | self.patch.stop() 34 | super(TestICMPCheckRun, self).tearDown() 35 | 36 | def test_icmp_run_use_instance_address(self): 37 | self.icmp_check.run() 38 | args = ['ping', '-c', '1', u'1.2.3.4'] 39 | self.mock_check_output.assert_called_once_with(args, shell=False, stderr=-2) 40 | 41 | def test_icmp_run_success(self): 42 | checkresults = self.icmp_check.statuscheckresult_set.all() 43 | self.assertEqual(len(checkresults), 0) 44 | self.icmp_check.run() 45 | checkresults = self.icmp_check.statuscheckresult_set.all() 46 | self.assertEqual(len(checkresults), 1) 47 | self.assertTrue(self.icmp_check.last_result().succeeded) 48 | 49 | def test_icmp_run_bad_address(self): 50 | self.mock_check_output.side_effect = subprocess.CalledProcessError(2, None, "ping: bad address") 51 | checkresults = self.icmp_check.statuscheckresult_set.all() 52 | self.assertEqual(len(checkresults), 0) 53 | self.icmp_check.run() 54 | checkresults = self.icmp_check.statuscheckresult_set.all() 55 | self.assertEqual(len(checkresults), 1) 56 | self.assertFalse(self.icmp_check.last_result().succeeded) 57 | 58 | def test_icmp_run_inacessible(self): 59 | self.mock_check_output.side_effect = subprocess.CalledProcessError(1, None, "packet loss") 60 | checkresults = self.icmp_check.statuscheckresult_set.all() 61 | self.assertEqual(len(checkresults), 0) 62 | self.icmp_check.run() 63 | checkresults = self.icmp_check.statuscheckresult_set.all() 64 | self.assertEqual(len(checkresults), 1) 65 | self.assertFalse(self.icmp_check.last_result().succeeded) 66 | -------------------------------------------------------------------------------- /cabot/cabotapp/tests/tests_jenkins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from datetime import timedelta 5 | 6 | import jenkins 7 | from cabot.cabotapp import jenkins as cabot_jenkins 8 | from cabot.cabotapp.models import JenkinsConfig 9 | from cabot.cabotapp.models.jenkins_check_plugin import JenkinsStatusCheck 10 | from django.utils import timezone 11 | from freezegun import freeze_time 12 | from mock import create_autospec, patch 13 | 14 | 15 | class TestGetStatus(unittest.TestCase): 16 | 17 | def setUp(self): 18 | self.job = { 19 | u'inQueue': False, 20 | u'queueItem': None, 21 | u'lastSuccessfulBuild': { 22 | u'number': 12, 23 | }, 24 | u'lastCompletedBuild': { 25 | u'number': 12, 26 | }, 27 | u'lastBuild': { 28 | u'number': 12, 29 | }, 30 | u'color': 'blue' 31 | } 32 | 33 | self.build = { 34 | u'number': 12, 35 | u'result': u'SUCCESS' 36 | 37 | } 38 | 39 | self.mock_client = create_autospec(jenkins.Jenkins) 40 | self.mock_client.get_job_info.return_value = self.job 41 | self.mock_client.get_build_info.return_value = self.build 42 | 43 | self.mock_config = create_autospec(JenkinsConfig) 44 | 45 | @patch("cabot.cabotapp.jenkins._get_jenkins_client") 46 | def test_job_passing(self, mock_jenkins): 47 | mock_jenkins.return_value = self.mock_client 48 | 49 | status = cabot_jenkins.get_job_status(self.mock_config, 'foo') 50 | 51 | expected = { 52 | 'active': True, 53 | 'succeeded': True, 54 | 'job_number': 12, 55 | 'blocked_build_time': None, 56 | 'consecutive_failures': 0, 57 | 'status_code': 200 58 | } 59 | self.assertEqual(status, expected) 60 | 61 | @patch("cabot.cabotapp.jenkins._get_jenkins_client") 62 | def test_job_failing(self, mock_jenkins): 63 | mock_jenkins.return_value = self.mock_client 64 | 65 | self.build[u'result'] = u'FAILURE' 66 | self.job[u'lastSuccessfulBuild'] = { 67 | u'number': 11, 68 | u'result': u'SUCCESS' 69 | } 70 | 71 | jenkins_check = JenkinsStatusCheck( 72 | name="foo", 73 | jenkins_config=JenkinsConfig( 74 | name="name", 75 | jenkins_api="a", 76 | jenkins_user="u", 77 | jenkins_pass="p" 78 | ) 79 | ) 80 | result = JenkinsStatusCheck._run(jenkins_check) 81 | 82 | self.assertEqual(result.consecutive_failures, 1) 83 | self.assertFalse(result.succeeded) 84 | 85 | @freeze_time('2017-03-02 10:30') 86 | @patch("cabot.cabotapp.jenkins._get_jenkins_client") 87 | def test_job_queued_last_succeeded(self, mock_jenkins): 88 | mock_jenkins.return_value = self.mock_client 89 | self.job[u'lastBuild'] = {u'number': 13} 90 | 91 | self.job[u'inQueue'] = True 92 | self.job['queueItem'] = { 93 | 'inQueueSince': float(timezone.now().strftime('%s')) * 1000 94 | } 95 | 96 | with freeze_time(timezone.now() + timedelta(minutes=10)): 97 | status = cabot_jenkins.get_job_status(self.mock_config, 'foo') 98 | 99 | expected = { 100 | 'active': True, 101 | 'succeeded': True, 102 | 'job_number': 12, 103 | 'queued_job_number': 13, 104 | 'blocked_build_time': 600, 105 | 'consecutive_failures': 0, 106 | 'status_code': 200 107 | } 108 | self.assertEqual(status, expected) 109 | 110 | @freeze_time('2017-03-02 10:30') 111 | @patch("cabot.cabotapp.jenkins._get_jenkins_client") 112 | def test_job_queued_last_failed(self, mock_jenkins): 113 | mock_jenkins.return_value = self.mock_client 114 | self.job[u'lastBuild'] = {u'number': 13} 115 | self.job[u'inQueue'] = True 116 | self.job['queueItem'] = { 117 | 'inQueueSince': float(timezone.now().strftime('%s')) * 1000 118 | } 119 | self.build[u'result'] = u'FAILURE' 120 | 121 | with freeze_time(timezone.now() + timedelta(minutes=10)): 122 | status = cabot_jenkins.get_job_status(self.mock_config, 'foo') 123 | 124 | expected = { 125 | 'active': True, 126 | 'succeeded': False, 127 | 'job_number': 12, 128 | 'queued_job_number': 13, 129 | 'blocked_build_time': 600, 130 | 'consecutive_failures': 0, 131 | 'status_code': 200 132 | } 133 | self.assertEqual(status, expected) 134 | 135 | @patch("cabot.cabotapp.jenkins._get_jenkins_client") 136 | def test_job_unknown(self, mock_jenkins): 137 | self.mock_client.get_job_info.side_effect = jenkins.NotFoundException() 138 | mock_jenkins.return_value = self.mock_client 139 | 140 | status = cabot_jenkins.get_job_status(self.mock_config, 'unknown-job') 141 | 142 | expected = { 143 | 'active': None, 144 | 'succeeded': None, 145 | 'job_number': None, 146 | 'blocked_build_time': None, 147 | 'status_code': 404 148 | } 149 | self.assertEqual(status, expected) 150 | 151 | @patch("cabot.cabotapp.jenkins._get_jenkins_client") 152 | def test_job_no_build(self, mock_jenkins): 153 | unbuilt_job = { 154 | u'inQueue': False, 155 | u'queueItem': None, 156 | u'lastSuccessfulBuild': None, 157 | u'lastCompletedBuild': None, 158 | u'lastBuild': None, 159 | u'color': u'notbuilt' 160 | } 161 | self.mock_client.get_job_info.return_value = unbuilt_job 162 | mock_jenkins.return_value = self.mock_client 163 | with self.assertRaises(Exception): 164 | cabot_jenkins.get_job_status(self.mock_config, 'job-unbuilt') 165 | 166 | @patch("cabot.cabotapp.jenkins._get_jenkins_client") 167 | def test_job_no_good_build(self, mock_jenkins): 168 | self.mock_client.get_job_info.return_value = { 169 | u'inQueue': False, 170 | u'queueItem': None, 171 | u'lastSuccessfulBuild': None, 172 | u'lastCompletedBuild': { 173 | u'number': 1, 174 | }, 175 | u'lastBuild': { 176 | u'number': 1, 177 | }, 178 | u'color': u'red' 179 | } 180 | self.mock_client.get_build_info.return_value = { 181 | u'number': 1, 182 | u'result': u'FAILURE' 183 | } 184 | mock_jenkins.return_value = self.mock_client 185 | status = cabot_jenkins.get_job_status(self.mock_config, 'job-no-good-build') 186 | expected = { 187 | 'active': True, 188 | 'succeeded': False, 189 | 'job_number': 1, 190 | 'blocked_build_time': None, 191 | 'consecutive_failures': 1, 192 | 'status_code': 200 193 | } 194 | self.assertEqual(status, expected) 195 | -------------------------------------------------------------------------------- /cabot/cabotapp/utils.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | 3 | 4 | def cabot_needs_setup(): 5 | return not get_user_model().objects.all().exists() 6 | -------------------------------------------------------------------------------- /cabot/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | from datetime import timedelta 5 | 6 | from django.conf import settings 7 | from celery import Celery 8 | 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cabot.settings') 10 | 11 | app = Celery('cabot') 12 | app.config_from_object('cabot.celeryconfig') 13 | app.autodiscover_tasks() 14 | 15 | app.conf.beat_schedule = { 16 | 'run-all-checks': { 17 | 'task': 'cabot.cabotapp.tasks.run_all_checks', 18 | 'schedule': timedelta(seconds=60), 19 | }, 20 | 'update-shifts': { 21 | 'task': 'cabot.cabotapp.tasks.update_shifts', 22 | 'schedule': timedelta(seconds=1800), 23 | }, 24 | 'clean-db': { 25 | 'task': 'cabot.cabotapp.tasks.clean_db', 26 | 'schedule': timedelta(seconds=60 * 60 * 24), 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /cabot/celeryconfig.py: -------------------------------------------------------------------------------- 1 | import os 2 | from cabot.settings_utils import environ_get_list 3 | 4 | broker_url = environ_get_list(['CELERY_BROKER_URL', 'CACHE_URL']) 5 | # Set environment variable if you want to run tests without a redis instance 6 | 7 | task_always_eager = environ_get_list(['CELERY_ALWAYS_EAGER', 'CELERY_TASK_ALWAYS_EAGER'], False) 8 | backend = os.environ.get('CELERY_RESULT_BACKEND', None) 9 | task_default_queue = os.environ.get('CELERY_DEFAULT_QUEUE', 'celery') 10 | 11 | timezone = 'UTC' 12 | -------------------------------------------------------------------------------- /cabot/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def global_settings(request): 5 | return { 6 | 'ENABLE_SUBSCRIPTION': settings.ENABLE_SUBSCRIPTION, 7 | 'ENABLE_DUTY_ROTA': settings.ENABLE_DUTY_ROTA, 8 | } 9 | -------------------------------------------------------------------------------- /cabot/entrypoint.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | def main(): 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cabot.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /cabot/rest_urls.py: -------------------------------------------------------------------------------- 1 | from django.db import models as model_fields 2 | from django.conf import settings 3 | from django.conf.urls import url, include 4 | from django.contrib.auth import models as django_models 5 | 6 | from polymorphic.models import PolymorphicModel 7 | from cabot.cabotapp import models, alert 8 | from rest_framework import routers, serializers, viewsets, mixins 9 | import logging 10 | 11 | logger = logging.getLogger(__name__) 12 | router = routers.DefaultRouter() 13 | 14 | 15 | def create_viewset(arg_model, arg_fields, arg_read_only_fields=(), readonly=False): 16 | arg_read_only_fields = ('id',) + arg_read_only_fields 17 | for field in arg_read_only_fields: 18 | if field not in arg_fields: 19 | arg_fields = arg_fields + (field,) 20 | 21 | class Serializer(serializers.ModelSerializer): 22 | class Meta: 23 | model = arg_model 24 | fields = arg_fields 25 | read_only_fields = arg_read_only_fields 26 | 27 | viewset_class = None 28 | if readonly: 29 | viewset_class = viewsets.ReadOnlyModelViewSet 30 | else: 31 | viewset_class = viewsets.ModelViewSet 32 | 33 | class ViewSet(viewset_class): 34 | queryset = arg_model.objects 35 | serializer_class = Serializer 36 | ordering = ['id'] 37 | filter_fields = arg_fields 38 | 39 | return ViewSet 40 | 41 | check_group_mixin_fields = ( 42 | 'name', 43 | 'users_to_notify', 44 | 'alerts_enabled', 45 | 'status_checks', 46 | 'alerts', 47 | 'hackpad_id', 48 | ) 49 | 50 | router.register(r'services', create_viewset( 51 | arg_model=models.Service, 52 | arg_fields=check_group_mixin_fields + ( 53 | 'url', 54 | 'instances', 55 | 'overall_status', 56 | ), 57 | )) 58 | 59 | router.register(r'instances', create_viewset( 60 | arg_model=models.Instance, 61 | arg_fields=check_group_mixin_fields + ( 62 | 'address', 63 | 'overall_status', 64 | ), 65 | )) 66 | 67 | status_check_fields = ( 68 | 'name', 69 | 'active', 70 | 'importance', 71 | 'frequency', 72 | 'debounce', 73 | 'calculated_status', 74 | ) 75 | 76 | router.register(r'status_checks', create_viewset( 77 | arg_model=models.StatusCheck, 78 | arg_fields=status_check_fields, 79 | readonly=True, 80 | )) 81 | 82 | router.register(r'icmp_checks', create_viewset( 83 | arg_model=models.ICMPStatusCheck, 84 | arg_fields=status_check_fields, 85 | )) 86 | 87 | router.register(r'graphite_checks', create_viewset( 88 | arg_model=models.GraphiteStatusCheck, 89 | arg_fields=status_check_fields + ( 90 | 'metric', 91 | 'check_type', 92 | 'value', 93 | 'expected_num_hosts', 94 | 'allowed_num_failures', 95 | ), 96 | )) 97 | 98 | router.register(r'http_checks', create_viewset( 99 | arg_model=models.HttpStatusCheck, 100 | arg_fields=status_check_fields + ( 101 | 'endpoint', 102 | 'username', 103 | 'password', 104 | 'text_match', 105 | 'status_code', 106 | 'timeout', 107 | 'verify_ssl_certificate', 108 | ), 109 | )) 110 | 111 | router.register(r'jenkins_checks', create_viewset( 112 | arg_model=models.JenkinsStatusCheck, 113 | arg_fields=status_check_fields + ( 114 | 'max_queued_build_time', 115 | 'jenkins_config', 116 | ), 117 | )) 118 | 119 | # User API is off by default, could expose/allow modifying dangerous fields 120 | if settings.EXPOSE_USER_API: 121 | router.register(r'users', create_viewset( 122 | arg_model=django_models.User, 123 | arg_fields=( 124 | 'password', 125 | 'is_active', 126 | 'groups', 127 | #'user_permissions', # Doesn't work, removing for now 128 | 'username', 129 | 'first_name', 130 | 'last_name', 131 | 'email', 132 | ), 133 | )) 134 | 135 | router.register(r'user_profiles', create_viewset( 136 | arg_model=models.UserProfile, 137 | arg_fields=( 138 | 'user', 139 | 'fallback_alert_user', 140 | ), 141 | )) 142 | 143 | 144 | router.register(r'shifts', create_viewset( 145 | arg_model=models.Shift, 146 | arg_fields=( 147 | 'start', 148 | 'end', 149 | 'user', 150 | 'uid', 151 | 'deleted', 152 | ) 153 | )) 154 | 155 | router.register(r'alertplugins', create_viewset( 156 | arg_model=alert.AlertPlugin, 157 | arg_fields=( 158 | 'title', 159 | ), 160 | readonly=True 161 | )) 162 | -------------------------------------------------------------------------------- /cabot/settings_ldap.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ldap 3 | from django_auth_ldap.config import LDAPSearch, GroupOfNamesType 4 | 5 | 6 | # Baseline configuration. 7 | AUTH_LDAP_SERVER_URI = os.environ.get('AUTH_LDAP_SERVER_URI', 'ldap://ldap.example.com') 8 | 9 | AUTH_LDAP_BIND_DN = os.environ.get('AUTH_LDAP_BIND_DN', 'cn=Manager,dc=example,dc=com') 10 | AUTH_LDAP_BIND_PASSWORD = os.environ.get('AUTH_LDAP_BIND_PASSWORD', '') 11 | AUTH_LDAP_USER_FILTER = os.environ.get('AUTH_LDAP_USER_FILTER', '(uid=%(user)s)') 12 | AUTH_LDAP_USER_SEARCH = LDAPSearch(os.environ.get('AUTH_LDAP_USER_SEARCH', 'ou=user,dc=example,dc=com'), 13 | ldap.SCOPE_SUBTREE, AUTH_LDAP_USER_FILTER) 14 | 15 | # Populate the Django user from the LDAP directory. 16 | AUTH_LDAP_USER_ATTR_MAP = { 17 | 'first_name': 'givenName', 18 | 'last_name': 'sn', 19 | 'email': 'mail', 20 | } 21 | -------------------------------------------------------------------------------- /cabot/settings_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from distutils.util import strtobool 3 | 4 | 5 | def force_bool(val): 6 | return strtobool(str(val)) 7 | 8 | 9 | def environ_get_list(names, default=None): 10 | for name in names: 11 | if name in os.environ: 12 | return os.environ[name] 13 | return default 14 | -------------------------------------------------------------------------------- /cabot/static/404.html: -------------------------------------------------------------------------------- 1 | 404 Error: Page not found -------------------------------------------------------------------------------- /cabot/static/500.html: -------------------------------------------------------------------------------- 1 | 500 error -------------------------------------------------------------------------------- /cabot/static/502.html: -------------------------------------------------------------------------------- 1 | 502 error -------------------------------------------------------------------------------- /cabot/static/503.html: -------------------------------------------------------------------------------- 1 | 503 error -------------------------------------------------------------------------------- /cabot/static/504.html: -------------------------------------------------------------------------------- 1 | 504 error -------------------------------------------------------------------------------- /cabot/static/arachnys/css/base.less: -------------------------------------------------------------------------------- 1 | body{ 2 | padding-top:50px; 3 | } 4 | .form-group { 5 | ul { 6 | list-style-type: none; 7 | padding: 5px 0 0 0; 8 | } 9 | 10 | input[type="radio"] { 11 | margin-top: -3px; 12 | } 13 | } 14 | 15 | tr.warning a { 16 | text-decoration: line-through; 17 | } 18 | 19 | #graphite_data_container { 20 | max-height: 200px; 21 | overflow: scroll; 22 | } 23 | 24 | .ui-autocomplete { 25 | max-height: 400px; 26 | overflow-y: scroll; 27 | font-size: 10px; 28 | } 29 | 30 | .jqstooltip { 31 | display: none; 32 | } 33 | 34 | div.dataTables_paginate { 35 | float: right; 36 | } 37 | 38 | div.dataTables_info { 39 | float: left; 40 | margin: 20px 0; 41 | } 42 | 43 | div.dataTables_length { 44 | float: right; 45 | } 46 | 47 | .messages { 48 | margin: auto; 49 | width: 50%; 50 | margin-top: 10px; 51 | } 52 | 53 | .navbar-brand > img { 54 | display: inline-block; 55 | } -------------------------------------------------------------------------------- /cabot/static/arachnys/css/morris.css: -------------------------------------------------------------------------------- 1 | .morris-hover{position:absolute;z-index:1000}.morris-hover.morris-default-style{border-radius:10px;padding:6px;color:#666;background:rgba(255,255,255,0.8);border:solid 2px rgba(230,230,230,0.8);font-family:sans-serif;font-size:12px;text-align:center}.morris-hover.morris-default-style .morris-hover-row-label{font-weight:bold;margin:0.25em 0} 2 | .morris-hover.morris-default-style .morris-hover-point{white-space:nowrap;margin:0.1em 0} 3 | -------------------------------------------------------------------------------- /cabot/static/arachnys/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/arachnys/img/favicon.ico -------------------------------------------------------------------------------- /cabot/static/arachnys/img/icon_48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/arachnys/img/icon_48x48.png -------------------------------------------------------------------------------- /cabot/static/arachnys/img/icon_96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/arachnys/img/icon_96x96.png -------------------------------------------------------------------------------- /cabot/static/bootstrap/css/dashboard.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Base structure 3 | */ 4 | 5 | /* Move down content because we have a fixed navbar that is 50px tall */ 6 | body { 7 | padding-top: 50px; 8 | } 9 | 10 | 11 | /* 12 | * Global add-ons 13 | */ 14 | 15 | .sub-header { 16 | padding-bottom: 10px; 17 | border-bottom: 1px solid #eee; 18 | } 19 | 20 | /* 21 | * Top navigation 22 | * Hide default border to remove 1px line. 23 | */ 24 | .navbar-fixed-top { 25 | border: 0; 26 | } 27 | 28 | /* 29 | * Sidebar 30 | */ 31 | 32 | /* Hide for mobile, show later */ 33 | .sidebar { 34 | display: none; 35 | } 36 | @media (min-width: 768px) { 37 | .sidebar { 38 | position: fixed; 39 | top: 51px; 40 | bottom: 0; 41 | left: 0; 42 | z-index: 1000; 43 | display: block; 44 | padding: 20px; 45 | overflow-x: hidden; 46 | overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ 47 | background-color: #f5f5f5; 48 | border-right: 1px solid #eee; 49 | } 50 | } 51 | 52 | /* Sidebar navigation */ 53 | .nav-sidebar { 54 | margin-right: -21px; /* 20px padding + 1px border */ 55 | margin-bottom: 20px; 56 | margin-left: -20px; 57 | } 58 | .nav-sidebar > li > a { 59 | padding-right: 20px; 60 | padding-left: 20px; 61 | } 62 | .nav-sidebar > .active > a, 63 | .nav-sidebar > .active > a:hover, 64 | .nav-sidebar > .active > a:focus { 65 | color: #fff; 66 | background-color: #428bca; 67 | } 68 | 69 | 70 | /* 71 | * Main content 72 | */ 73 | 74 | .main { 75 | padding: 20px; 76 | } 77 | @media (min-width: 768px) { 78 | .main { 79 | padding-right: 40px; 80 | padding-left: 40px; 81 | } 82 | } 83 | .main .page-header { 84 | margin-top: 0; 85 | } 86 | 87 | 88 | /* 89 | * Placeholder dashboard ideas 90 | */ 91 | 92 | .placeholders { 93 | margin-bottom: 30px; 94 | text-align: center; 95 | } 96 | .placeholders h4 { 97 | margin-bottom: 0; 98 | } 99 | .placeholder { 100 | margin-bottom: 20px; 101 | } 102 | .placeholder img { 103 | display: inline-block; 104 | border-radius: 50%; 105 | } 106 | -------------------------------------------------------------------------------- /cabot/static/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /cabot/static/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /cabot/static/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /cabot/static/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /cabot/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/favicon.ico -------------------------------------------------------------------------------- /cabot/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /cabot/static/theme/css/bootstrap-datatables.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:75px;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:8px;white-space:nowrap}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:30px}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{position:absolute;bottom:8px;right:8px;display:block;font-family:'Glyphicons Halflings';opacity:0.5}table.dataTable thead .sorting:after{opacity:0.2;content:"\e150"}table.dataTable thead .sorting_asc:after{content:"\e155"}table.dataTable thead .sorting_desc:after{content:"\e156"}table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{color:#eee}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody>table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody>table>thead .sorting:after,div.dataTables_scrollBody>table>thead .sorting_asc:after,div.dataTables_scrollBody>table>thead .sorting_desc:after{display:none}div.dataTables_scrollBody>table>tbody>tr:first-child>th,div.dataTables_scrollBody>table>tbody>tr:first-child>td{border-top:none}div.dataTables_scrollFoot>.dataTables_scrollFootInner{box-sizing:content-box}div.dataTables_scrollFoot>.dataTables_scrollFootInner>table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}}table.dataTable.table-condensed>thead>tr>th{padding-right:20px}table.dataTable.table-condensed .sorting:after,table.dataTable.table-condensed .sorting_asc:after,table.dataTable.table-condensed .sorting_desc:after{top:6px;right:6px}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:0}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:last-child{padding-right:0} 2 | -------------------------------------------------------------------------------- /cabot/static/theme/css/chosen-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/css/chosen-sprite.png -------------------------------------------------------------------------------- /cabot/static/theme/css/chosen-sprite@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/css/chosen-sprite@2x.png -------------------------------------------------------------------------------- /cabot/static/theme/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /cabot/static/theme/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /cabot/static/theme/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /cabot/static/theme/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /cabot/static/theme/img/animated-overlay.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/animated-overlay.gif -------------------------------------------------------------------------------- /cabot/static/theme/img/arrows-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/arrows-active.png -------------------------------------------------------------------------------- /cabot/static/theme/img/arrows-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/arrows-normal.png -------------------------------------------------------------------------------- /cabot/static/theme/img/bg-input-focus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/bg-input-focus.png -------------------------------------------------------------------------------- /cabot/static/theme/img/bg-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/bg-input.png -------------------------------------------------------------------------------- /cabot/static/theme/img/bg-login.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/bg-login.jpg -------------------------------------------------------------------------------- /cabot/static/theme/img/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/bg.jpg -------------------------------------------------------------------------------- /cabot/static/theme/img/buttons.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/buttons.gif -------------------------------------------------------------------------------- /cabot/static/theme/img/calendar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/calendar.gif -------------------------------------------------------------------------------- /cabot/static/theme/img/chat-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/chat-left.png -------------------------------------------------------------------------------- /cabot/static/theme/img/chat-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/chat-right.png -------------------------------------------------------------------------------- /cabot/static/theme/img/chosen-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/chosen-sprite.png -------------------------------------------------------------------------------- /cabot/static/theme/img/close-button-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/close-button-white.png -------------------------------------------------------------------------------- /cabot/static/theme/img/close-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/close-button.png -------------------------------------------------------------------------------- /cabot/static/theme/img/crop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/crop.gif -------------------------------------------------------------------------------- /cabot/static/theme/img/dbg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/dbg.jpg -------------------------------------------------------------------------------- /cabot/static/theme/img/dialogs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/dialogs.png -------------------------------------------------------------------------------- /cabot/static/theme/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/favicon.ico -------------------------------------------------------------------------------- /cabot/static/theme/img/glyphicons-halflings-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/glyphicons-halflings-red.png -------------------------------------------------------------------------------- /cabot/static/theme/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /cabot/static/theme/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /cabot/static/theme/img/i_16_radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/i_16_radio.png -------------------------------------------------------------------------------- /cabot/static/theme/img/icons-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/icons-big.png -------------------------------------------------------------------------------- /cabot/static/theme/img/icons-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/icons-small.png -------------------------------------------------------------------------------- /cabot/static/theme/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/logo.png -------------------------------------------------------------------------------- /cabot/static/theme/img/logo20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/logo20.png -------------------------------------------------------------------------------- /cabot/static/theme/img/progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/progress.gif -------------------------------------------------------------------------------- /cabot/static/theme/img/quicklook-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/quicklook-bg.png -------------------------------------------------------------------------------- /cabot/static/theme/img/quicklook-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/quicklook-icons.png -------------------------------------------------------------------------------- /cabot/static/theme/img/resize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/resize.png -------------------------------------------------------------------------------- /cabot/static/theme/img/spinner-mini.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/spinner-mini.gif -------------------------------------------------------------------------------- /cabot/static/theme/img/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/sprite.png -------------------------------------------------------------------------------- /cabot/static/theme/img/toolbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/toolbar.gif -------------------------------------------------------------------------------- /cabot/static/theme/img/toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/toolbar.png -------------------------------------------------------------------------------- /cabot/static/theme/img/ui-bg_flat_0_aaaaaa_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/ui-bg_flat_0_aaaaaa_40x100.png -------------------------------------------------------------------------------- /cabot/static/theme/img/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /cabot/static/theme/img/ui-bg_glass_55_fbf9ee_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/ui-bg_glass_55_fbf9ee_1x400.png -------------------------------------------------------------------------------- /cabot/static/theme/img/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /cabot/static/theme/img/ui-bg_glass_75_dadada_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/ui-bg_glass_75_dadada_1x400.png -------------------------------------------------------------------------------- /cabot/static/theme/img/ui-bg_glass_75_e6e6e6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/ui-bg_glass_75_e6e6e6_1x400.png -------------------------------------------------------------------------------- /cabot/static/theme/img/ui-bg_glass_95_fef1ec_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/ui-bg_glass_95_fef1ec_1x400.png -------------------------------------------------------------------------------- /cabot/static/theme/img/ui-bg_highlight-soft_75_cccccc_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/ui-bg_highlight-soft_75_cccccc_1x100.png -------------------------------------------------------------------------------- /cabot/static/theme/img/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /cabot/static/theme/img/ui-icons_2e83ff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/ui-icons_2e83ff_256x240.png -------------------------------------------------------------------------------- /cabot/static/theme/img/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /cabot/static/theme/img/ui-icons_888888_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/ui-icons_888888_256x240.png -------------------------------------------------------------------------------- /cabot/static/theme/img/ui-icons_cd0a0a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arachnys/cabot/eb0b3544f8c8ab2dee4643df191da346a941734f/cabot/static/theme/img/ui-icons_cd0a0a_256x240.png -------------------------------------------------------------------------------- /cabot/static/theme/js/custom.js: -------------------------------------------------------------------------------- 1 | 2 | /* ---------- Additional functions for data table ---------- */ 3 | $.fn.dataTableExt.oApi.fnPagingInfo = function ( oSettings ) 4 | { 5 | return { 6 | "iStart": oSettings._iDisplayStart, 7 | "iEnd": oSettings.fnDisplayEnd(), 8 | "iLength": oSettings._iDisplayLength, 9 | "iTotal": oSettings.fnRecordsTotal(), 10 | "iFilteredTotal": oSettings.fnRecordsDisplay(), 11 | "iPage": Math.ceil( oSettings._iDisplayStart / oSettings._iDisplayLength ), 12 | "iTotalPages": Math.ceil( oSettings.fnRecordsDisplay() / oSettings._iDisplayLength ) 13 | }; 14 | } 15 | $.extend( $.fn.dataTableExt.oPagination, { 16 | "bootstrap": { 17 | "fnInit": function( oSettings, nPaging, fnDraw ) { 18 | var oLang = oSettings.oLanguage.oPaginate; 19 | var fnClickHandler = function ( e ) { 20 | e.preventDefault(); 21 | if ( oSettings.oApi._fnPageChange(oSettings, e.data.action) ) { 22 | fnDraw( oSettings ); 23 | } 24 | }; 25 | 26 | $(nPaging).append( 27 | '' 31 | ); 32 | var els = $('a', nPaging); 33 | $(els[0]).bind( 'click.DT', { action: "previous" }, fnClickHandler ); 34 | $(els[1]).bind( 'click.DT', { action: "next" }, fnClickHandler ); 35 | }, 36 | 37 | "fnUpdate": function ( oSettings, fnDraw ) { 38 | var iListLength = 5; 39 | var oPaging = oSettings.oInstance.fnPagingInfo(); 40 | var an = oSettings.aanFeatures.p; 41 | var i, j, sClass, iStart, iEnd, iHalf=Math.floor(iListLength/2); 42 | 43 | if ( oPaging.iTotalPages < iListLength) { 44 | iStart = 1; 45 | iEnd = oPaging.iTotalPages; 46 | } 47 | else if ( oPaging.iPage <= iHalf ) { 48 | iStart = 1; 49 | iEnd = iListLength; 50 | } else if ( oPaging.iPage >= (oPaging.iTotalPages-iHalf) ) { 51 | iStart = oPaging.iTotalPages - iListLength + 1; 52 | iEnd = oPaging.iTotalPages; 53 | } else { 54 | iStart = oPaging.iPage - iHalf + 1; 55 | iEnd = iStart + iListLength - 1; 56 | } 57 | 58 | for ( i=0, iLen=an.length ; i'+j+'') 66 | .insertBefore( $('li:last', an[i])[0] ) 67 | .bind('click', function (e) { 68 | e.preventDefault(); 69 | oSettings._iDisplayStart = (parseInt($('a', this).text(),10)-1) * oPaging.iLength; 70 | fnDraw( oSettings ); 71 | } ); 72 | } 73 | 74 | // add / remove disabled classes from the static elements 75 | if ( oPaging.iPage === 0 ) { 76 | $('li:first', an[i]).addClass('disabled'); 77 | } else { 78 | $('li:first', an[i]).removeClass('disabled'); 79 | } 80 | 81 | if ( oPaging.iPage === oPaging.iTotalPages-1 || oPaging.iTotalPages === 0 ) { 82 | $('li:last', an[i]).addClass('disabled'); 83 | } else { 84 | $('li:last', an[i]).removeClass('disabled'); 85 | } 86 | } 87 | } 88 | } 89 | }); -------------------------------------------------------------------------------- /cabot/static/theme/js/jquery.dataTables.bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | DataTables Bootstrap 3 integration 3 | ©2011-2015 SpryMedia Ltd - datatables.net/license 4 | */ 5 | (function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return b(a,window,document)}):"object"===typeof exports?module.exports=function(a,d){a||(a=window);if(!d||!d.fn.dataTable)d=require("datatables.net")(a,d).$;return b(d,a,a.document)}:b(jQuery,window,document)})(function(b,a,d,m){var f=b.fn.dataTable;b.extend(!0,f.defaults,{dom:"<'row'<'col-sm-6'l><'col-sm-6'f>><'row'<'col-sm-12'tr>><'row'<'col-sm-5'i><'col-sm-7'p>>",renderer:"bootstrap"});b.extend(f.ext.classes, 6 | {sWrapper:"dataTables_wrapper form-inline dt-bootstrap",sFilterInput:"form-control input-sm",sLengthSelect:"form-control input-sm",sProcessing:"dataTables_processing panel panel-default"});f.ext.renderer.pageButton.bootstrap=function(a,h,r,s,j,n){var o=new f.Api(a),t=a.oClasses,k=a.oLanguage.oPaginate,u=a.oLanguage.oAria.paginate||{},e,g,p=0,q=function(d,f){var l,h,i,c,m=function(a){a.preventDefault();!b(a.currentTarget).hasClass("disabled")&&o.page()!=a.data.action&&o.page(a.data.action).draw("page")}; 7 | l=0;for(h=f.length;l",{"class":t.sPageButton+" "+g,id:0===r&&"string"===typeof c?a.sTableId+"_"+c:null}).append(b("",{href:"#", 8 | "aria-controls":a.sTableId,"aria-label":u[c],"data-dt-idx":p,tabindex:a.iTabIndex}).html(e)).appendTo(d),a.oApi._fnBindAction(i,{action:c},m),p++)}},i;try{i=b(h).find(d.activeElement).data("dt-idx")}catch(v){}q(b(h).empty().html('