├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── Screenshot.png ├── docker-compose.yml ├── frontend ├── .babelrc ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── README.md ├── build │ ├── build.js │ ├── check-versions.js │ ├── dev-client.js │ ├── dev-server.js │ ├── utils.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── config │ ├── base.env.js │ ├── dev.env.js │ ├── index.js │ ├── prod.env.js │ └── test.env.js ├── index.html ├── package.json ├── src │ ├── api.js │ ├── auth.js │ ├── components │ │ ├── App.vue │ │ ├── Service.vue │ │ ├── Services.vue │ │ ├── Settings.vue │ │ └── UserMenu.vue │ ├── config.js │ ├── main.js │ └── utils │ │ └── notifications.js ├── static │ ├── .gitkeep │ ├── favicon.ico │ ├── images │ │ ├── icons │ │ │ ├── atlassian.png │ │ │ ├── authorizedotnet.jpg │ │ │ ├── aws.png │ │ │ ├── azure.png │ │ │ ├── bitbucket.png │ │ │ ├── box.png │ │ │ ├── chargify.png │ │ │ ├── circleci.png │ │ │ ├── cloudflare.png │ │ │ ├── codeclimate.png │ │ │ ├── codeship.png │ │ │ ├── compose.png │ │ │ ├── datadog.png │ │ │ ├── disqus.png │ │ │ ├── dnsimple.png │ │ │ ├── do.png │ │ │ ├── docker.png │ │ │ ├── dropbox.png │ │ │ ├── duo.png │ │ │ ├── dyn.png │ │ │ ├── fastly.png │ │ │ ├── ftrack.png │ │ │ ├── gcloud.png │ │ │ ├── github.png │ │ │ ├── gitlab.png │ │ │ ├── gocardless.png │ │ │ ├── gotomeeting.png │ │ │ ├── harvest.png │ │ │ ├── hashicorp.png │ │ │ ├── heroku.png │ │ │ ├── honeybadger.png │ │ │ ├── linode.png │ │ │ ├── loggly.jpg │ │ │ ├── mailgun.png │ │ │ ├── maxcdn.png │ │ │ ├── newrelic.png │ │ │ ├── npm.png │ │ │ ├── opbeat.png │ │ │ ├── packagecloud.png │ │ │ ├── pagerduty.png │ │ │ ├── pingdom.png │ │ │ ├── pingidentity.png │ │ │ ├── pusher.png │ │ │ ├── pyinfra.png │ │ │ ├── quay.png │ │ │ ├── redislabs.png │ │ │ ├── rollbar.png │ │ │ ├── rubygems.png │ │ │ ├── sendgrid.png │ │ │ ├── sendwithus.png │ │ │ ├── sentry.png │ │ │ ├── shotgun.png │ │ │ ├── slack.png │ │ │ ├── sparkpost.png │ │ │ ├── statusio.png │ │ │ ├── statuspage.png │ │ │ ├── stormpath.png │ │ │ ├── stripe.png │ │ │ ├── travis.png │ │ │ ├── twilio.png │ │ │ ├── victorops.png │ │ │ ├── vultr.png │ │ │ └── weblate.png │ │ ├── loading.gif │ │ └── logo.png │ └── main.css └── yarn.lock ├── isserviceup ├── __init__.py ├── api │ ├── __init__.py │ ├── auth.py │ ├── status.py │ └── user.py ├── app.py ├── celeryapp.py ├── config │ ├── __init__.py │ ├── celery.py │ ├── config.py │ └── gunicorn.py ├── helpers │ ├── __init__.py │ ├── decorators.py │ ├── exceptions.py │ ├── github.py │ └── utils.py ├── managers.py ├── models │ ├── __init__.py │ ├── favorite.py │ └── user.py ├── notifiers │ ├── __init__.py │ ├── cachet.py │ ├── notifier.py │ └── slack.py ├── services │ ├── __init__.py │ ├── atlassian.py │ ├── authorizedotnet.py │ ├── aws.py │ ├── azure.py │ ├── bitbucket.py │ ├── box.py │ ├── chargify.py │ ├── circleci.py │ ├── cloudflare.py │ ├── codeclimate.py │ ├── codeship.py │ ├── compose.py │ ├── datadog.py │ ├── digitalocean.py │ ├── disqus.py │ ├── dnsimple.py │ ├── docker.py │ ├── dropbox.py │ ├── duo.py │ ├── dyn.py │ ├── fastly.py │ ├── ftrack.py │ ├── gcloud.py │ ├── github.py │ ├── gitlab.py │ ├── gocardless.py │ ├── gotomeeting.py │ ├── harvest.py │ ├── hashicorp.py │ ├── heroku.py │ ├── honeybadger.py │ ├── linode.py │ ├── loggly.py │ ├── mailgun.py │ ├── maxcdn.py │ ├── models │ │ ├── __init__.py │ │ ├── gitlab.py │ │ ├── service.py │ │ ├── statusio.py │ │ ├── statuspage.py │ │ └── weblate.py │ ├── newrelic.py │ ├── npm.py │ ├── opbeat.py │ ├── packagecloud.py │ ├── pagerduty.py │ ├── pingdom.py │ ├── pingidentity.py │ ├── pusher.py │ ├── pypi.py │ ├── quay.py │ ├── redislabs.py │ ├── rollbar.py │ ├── rubygems.py │ ├── sendgrid.py │ ├── sendwithus.py │ ├── sentry.py │ ├── shotgun.py │ ├── slack.py │ ├── sparkpost.py │ ├── statusio.py │ ├── statuspage.py │ ├── stormpath.py │ ├── stripe.py │ ├── travis.py │ ├── twilio.py │ ├── victorops.py │ ├── vultr.py │ └── weblate.py └── storage │ ├── __init__.py │ ├── favorites.py │ ├── services.py │ ├── sessions.py │ └── users.py ├── requirements.in ├── requirements.txt └── shared └── .gitkeep /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | frontend/node_modules/**/* 3 | frontend/node_modules 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.pyc 3 | .idea 4 | .env* 5 | celerybeat* 6 | static 7 | shared/* 8 | !shared/.gitkeep 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.5 2 | 3 | RUN useradd --user-group --create-home --shell /bin/false app 4 | 5 | ENV INSTALL_PATH /isserviceup 6 | RUN mkdir -p $INSTALL_PATH && chown app:app $INSTALL_PATH 7 | 8 | WORKDIR $INSTALL_PATH 9 | 10 | COPY requirements.txt requirements.txt 11 | RUN pip install -r requirements.txt 12 | 13 | USER app 14 | 15 | COPY . . 16 | 17 | CMD gunicorn -c "isserviceup/config/gunicorn.py" isserviceup.app:app 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IsServiceUp 2 | 3 | IsServiceUp helps you monitor all the cloud services you rely on in a single web page. 4 | 5 | You can customize it with the services you want to monitor and host it on your own server. 6 | 7 | ![](https://raw.githubusercontent.com/marcopaz/is-service-up/master/Screenshot.png) 8 | 9 | Sorry, Compose, bad timing :smile: 10 | 11 | ## How to run 12 | ### Using Docker 13 | `docker-compose up --build` 14 | 15 | and you're up and running! :sparkles: 16 | ### List of services 17 | You can customize very easily the list of services you want to monitor by editing the variable `SERVICES` in the [config file](https://github.com/marcopaz/is-service-up/blob/master/isserviceup/config/config.py). 18 | 19 | ## How to contribute 20 | If you want to add something to the project, please fork this repository and create a Pull request. 21 | 22 | ### Extend it with a new service 23 | 24 | The way services are handled is based on a _plugin_ system, so monitoring a new service is straightforward: you can either file a feature request and hope that someone will implement the plugin, or you can implement it yourself. If you do decide to implement a plugin for a new service, we'd be thankful if you could share it with everyone by creating a pull request on this repository...thanks! 25 | 26 | Of course, creating a plugin for a new service is only possible if the service exposes a status page. Find that out first. 27 | 28 | Next, figure out if the status page is built using [Atlassian StatusPage](https://www.statuspage.io/). If that's the case, check out [this commit](https://github.com/marcopaz/is-service-up/commit/39df5a9124a01d39d66e7637a297896827a4262e) for an example on how to create your plugin. 29 | 30 | If it's a more generic status page, then the implementation depends on the specific service. If you're lucky, the status will be exposed through a beautiful API, like in [the case of GitHub](https://github.com/marcopaz/is-service-up/blob/master/isserviceup/services/github.py). Otherwise, find your inspiration from all others services that we have implemented for you. Good luck! :satellite: 31 | 32 | ## Maintainers 33 | * [Marco Pazzaglia](https://github.com/marcopaz) 34 | * [Alessandro Cosentino](https://github.com/cosenal) 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/Screenshot.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | 5 | # frontend_builder: 6 | # build: frontend 7 | # volumes: 8 | # - './frontend/dist:/frontend/dist' 9 | # command: npm run build 10 | 11 | website: 12 | build: . 13 | ports: 14 | - "8000:8000" 15 | volumes: 16 | - '.:/isserviceup' 17 | depends_on: 18 | # - frontend_builder 19 | - redis 20 | - mongo 21 | command: gunicorn -c "isserviceup/config/gunicorn.py" isserviceup.app:app --reload 22 | 23 | redis: 24 | image: redis 25 | command: redis-server --requirepass devpassword --appendonly yes 26 | volumes: 27 | - 'redis:/data' 28 | 29 | mongo: 30 | image: 'mongo:3' 31 | volumes: 32 | - 'mongo:/data/db' 33 | 34 | celery_beat: 35 | build: . 36 | command: celery -A isserviceup.celeryapp.app beat 37 | volumes: 38 | - './isserviceup:/isserviceup/isserviceup' 39 | depends_on: 40 | - redis 41 | 42 | celery: 43 | build: . 44 | command: celery worker -l info -P gevent -c 50 -A isserviceup.celeryapp.app 45 | volumes: 46 | - './isserviceup:/isserviceup/isserviceup' 47 | - './shared:/home/app/shared' 48 | depends_on: 49 | - redis 50 | 51 | volumes: 52 | redis: 53 | mongo: 54 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false 5 | } 6 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | * 4 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | extends: 'airbnb-base', 8 | // required to lint *.vue files 9 | plugins: [ 10 | 'html' 11 | ], 12 | // check if imports actually resolve 13 | 'settings': { 14 | 'import/resolver': { 15 | 'webpack': { 16 | 'config': 'build/webpack.base.conf.js' 17 | } 18 | } 19 | }, 20 | // add your custom rules here 21 | 'rules': { 22 | // don't require .vue extension when importing 23 | 'import/extensions': ['error', 'always', { 24 | 'js': 'never', 25 | 'vue': 'never' 26 | }], 27 | // allow debugger during development 28 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | test/unit/coverage 6 | test/e2e/reports 7 | selenium-debug.log 8 | .idea 9 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | ENV INSTALL_PATH /frontend 4 | RUN mkdir -p $INSTALL_PATH 5 | WORKDIR $INSTALL_PATH 6 | 7 | RUN npm install -g yarn 8 | 9 | COPY package.json package.json 10 | COPY yarn.lock yarn.lock 11 | RUN yarn 12 | 13 | COPY . . 14 | RUN npm run build 15 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # isserviceup frontend 2 | 3 | ## Build Setup 4 | 5 | ``` bash 6 | # install dependencies 7 | npm install 8 | 9 | # serve with hot reload at localhost:8080 10 | npm run dev 11 | 12 | # build for production with minification 13 | npm run build 14 | ``` 15 | 16 | If you want to deploy it on a custom domain name change the `API_HOST` variable in the [config file](https://github.com/marcopaz/is-service-up/blob/master/frontend/src/config.js). 17 | 18 | Once you have build it for production you need to place the content of the dist folder in the static folder of the backend, see [scripts/build_frontend.sh](https://github.com/marcopaz/is-service-up/blob/master/scripts/build_frontend.sh). 19 | -------------------------------------------------------------------------------- /frontend/build/build.js: -------------------------------------------------------------------------------- 1 | // https://github.com/shelljs/shelljs 2 | require('./check-versions')() 3 | require('shelljs/global') 4 | env.NODE_ENV = 'production' 5 | 6 | var path = require('path') 7 | var config = require('../config') 8 | var ora = require('ora') 9 | var webpack = require('webpack') 10 | var webpackConfig = require('./webpack.prod.conf') 11 | 12 | console.log( 13 | ' Tip:\n' + 14 | ' Built files are meant to be served over an HTTP server.\n' + 15 | ' Opening index.html over file:// won\'t work.\n' 16 | ) 17 | 18 | var spinner = ora('building for production...') 19 | spinner.start() 20 | 21 | var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory) 22 | rm('-rf', assetsPath) 23 | mkdir('-p', assetsPath) 24 | cp('-R', 'static/*', assetsPath) 25 | 26 | webpack(webpackConfig, function (err, stats) { 27 | spinner.stop() 28 | if (err) throw err 29 | process.stdout.write(stats.toString({ 30 | colors: true, 31 | modules: false, 32 | children: false, 33 | chunks: false, 34 | chunkModules: false 35 | }) + '\n') 36 | }) 37 | -------------------------------------------------------------------------------- /frontend/build/check-versions.js: -------------------------------------------------------------------------------- 1 | var semver = require('semver') 2 | var chalk = require('chalk') 3 | var packageConfig = require('../package.json') 4 | var exec = function (cmd) { 5 | return require('child_process') 6 | .execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | { 16 | name: 'npm', 17 | currentVersion: exec('npm --version'), 18 | versionRequirement: packageConfig.engines.npm 19 | } 20 | ] 21 | 22 | module.exports = function () { 23 | var warnings = [] 24 | for (var i = 0; i < versionRequirements.length; i++) { 25 | var mod = versionRequirements[i] 26 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 27 | warnings.push(mod.name + ': ' + 28 | chalk.red(mod.currentVersion) + ' should be ' + 29 | chalk.green(mod.versionRequirement) 30 | ) 31 | } 32 | } 33 | 34 | if (warnings.length) { 35 | console.log('') 36 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 37 | console.log() 38 | for (var i = 0; i < warnings.length; i++) { 39 | var warning = warnings[i] 40 | console.log(' ' + warning) 41 | } 42 | console.log() 43 | process.exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /frontend/build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | var config = require('../config') 3 | if (!process.env.NODE_ENV) process.env.NODE_ENV = config.dev.env 4 | var path = require('path') 5 | var express = require('express') 6 | var webpack = require('webpack') 7 | var opn = require('opn') 8 | var proxyMiddleware = require('http-proxy-middleware') 9 | var webpackConfig = process.env.NODE_ENV === 'testing' 10 | ? require('./webpack.prod.conf') 11 | : require('./webpack.dev.conf') 12 | 13 | // default port where dev server listens for incoming traffic 14 | var port = process.env.PORT || config.dev.port 15 | // Define HTTP proxies to your custom API backend 16 | // https://github.com/chimurai/http-proxy-middleware 17 | var proxyTable = config.dev.proxyTable 18 | 19 | var app = express() 20 | var compiler = webpack(webpackConfig) 21 | 22 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 23 | publicPath: webpackConfig.output.publicPath, 24 | stats: { 25 | colors: true, 26 | chunks: false 27 | } 28 | }) 29 | 30 | var hotMiddleware = require('webpack-hot-middleware')(compiler) 31 | // force page reload when html-webpack-plugin template changes 32 | compiler.plugin('compilation', function (compilation) { 33 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 34 | hotMiddleware.publish({ action: 'reload' }) 35 | cb() 36 | }) 37 | }) 38 | 39 | // proxy api requests 40 | Object.keys(proxyTable).forEach(function (context) { 41 | var options = proxyTable[context] 42 | if (typeof options === 'string') { 43 | options = { target: options } 44 | } 45 | app.use(proxyMiddleware(context, options)) 46 | }) 47 | 48 | // handle fallback for HTML5 history API 49 | app.use(require('connect-history-api-fallback')()) 50 | 51 | // serve webpack bundle output 52 | app.use(devMiddleware) 53 | 54 | // enable hot-reload and state-preserving 55 | // compilation error display 56 | app.use(hotMiddleware) 57 | 58 | // serve pure static assets 59 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 60 | app.use(staticPath, express.static('./static')) 61 | 62 | module.exports = app.listen(port, function (err) { 63 | if (err) { 64 | console.log(err) 65 | return 66 | } 67 | var uri = 'http://localhost:' + port 68 | console.log('Listening at ' + uri + '\n') 69 | opn(uri) 70 | }) 71 | -------------------------------------------------------------------------------- /frontend/build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | // generate loader string to be used with extract text plugin 15 | function generateLoaders (loaders) { 16 | var sourceLoader = loaders.map(function (loader) { 17 | var extraParamChar 18 | if (/\?/.test(loader)) { 19 | loader = loader.replace(/\?/, '-loader?') 20 | extraParamChar = '&' 21 | } else { 22 | loader = loader + '-loader' 23 | extraParamChar = '?' 24 | } 25 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '') 26 | }).join('!') 27 | 28 | // Extract CSS when that option is specified 29 | // (which is the case during production build) 30 | if (options.extract) { 31 | return ExtractTextPlugin.extract('vue-style-loader', sourceLoader) 32 | } else { 33 | return ['vue-style-loader', sourceLoader].join('!') 34 | } 35 | } 36 | 37 | // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html 38 | return { 39 | css: generateLoaders(['css']), 40 | postcss: generateLoaders(['css']), 41 | less: generateLoaders(['css', 'less']), 42 | sass: generateLoaders(['css', 'sass?indentedSyntax']), 43 | scss: generateLoaders(['css', 'sass']), 44 | stylus: generateLoaders(['css', 'stylus']), 45 | styl: generateLoaders(['css', 'stylus']) 46 | } 47 | } 48 | 49 | // Generate loaders for standalone style files (outside of .vue) 50 | exports.styleLoaders = function (options) { 51 | var output = [] 52 | var loaders = exports.cssLoaders(options) 53 | for (var extension in loaders) { 54 | var loader = loaders[extension] 55 | output.push({ 56 | test: new RegExp('\\.' + extension + '$'), 57 | loader: loader 58 | }) 59 | } 60 | return output 61 | } 62 | -------------------------------------------------------------------------------- /frontend/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var utils = require('./utils') 4 | var projectRoot = path.resolve(__dirname, '../') 5 | 6 | var env = process.env.NODE_ENV 7 | // check env & config/index.js to decide weither to enable CSS Sourcemaps for the 8 | // various preprocessor loaders added to vue-loader at the end of this file 9 | var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap) 10 | var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap) 11 | var useCssSourceMap = cssSourceMapDev || cssSourceMapProd 12 | 13 | module.exports = { 14 | entry: { 15 | app: './src/main.js' 16 | }, 17 | output: { 18 | path: config.build.assetsRoot, 19 | publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath, 20 | filename: '[name].js' 21 | }, 22 | resolve: { 23 | extensions: ['', '.js', '.vue'], 24 | fallback: [path.join(__dirname, '../node_modules')], 25 | alias: { 26 | 'vue$': 'vue/dist/vue', 27 | 'src': path.resolve(__dirname, '../src'), 28 | 'assets': path.resolve(__dirname, '../src/assets'), 29 | 'components': path.resolve(__dirname, '../src/components') 30 | } 31 | }, 32 | resolveLoader: { 33 | fallback: [path.join(__dirname, '../node_modules')] 34 | }, 35 | module: { 36 | preLoaders: [ 37 | { 38 | test: /\.vue$/, 39 | loader: 'eslint', 40 | include: projectRoot, 41 | exclude: /node_modules/ 42 | }, 43 | { 44 | test: /\.js$/, 45 | loader: 'eslint', 46 | include: projectRoot, 47 | exclude: /node_modules/ 48 | } 49 | ], 50 | loaders: [ 51 | { 52 | test: /\.vue$/, 53 | loader: 'vue' 54 | }, 55 | { 56 | test: /\.js$/, 57 | loader: 'babel', 58 | include: projectRoot, 59 | exclude: /node_modules/ 60 | }, 61 | { 62 | test: /\.json$/, 63 | loader: 'json' 64 | }, 65 | { 66 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 67 | loader: 'url', 68 | query: { 69 | limit: 10000, 70 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 71 | } 72 | }, 73 | { 74 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 75 | loader: 'url', 76 | query: { 77 | limit: 10000, 78 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 79 | } 80 | } 81 | ] 82 | }, 83 | eslint: { 84 | formatter: require('eslint-friendly-formatter') 85 | }, 86 | vue: { 87 | loaders: utils.cssLoaders({ sourceMap: useCssSourceMap }), 88 | postcss: [ 89 | require('autoprefixer')({ 90 | browsers: ['last 2 versions'] 91 | }) 92 | ] 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /frontend/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var config = require('../config') 2 | var webpack = require('webpack') 3 | var merge = require('webpack-merge') 4 | var utils = require('./utils') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | 8 | // add hot-reload related code to entry chunks 9 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 10 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 11 | }) 12 | 13 | module.exports = merge(baseWebpackConfig, { 14 | module: { 15 | loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 16 | }, 17 | // eval-source-map is faster for development 18 | devtool: '#eval-source-map', 19 | plugins: [ 20 | new webpack.DefinePlugin({ 21 | 'process.env': config.dev.env 22 | }), 23 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 24 | new webpack.optimize.OccurenceOrderPlugin(), 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: 'index.html', 31 | inject: true 32 | }) 33 | ] 34 | }) 35 | -------------------------------------------------------------------------------- /frontend/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var env = process.env.NODE_ENV === 'testing' 10 | ? require('../config/test.env') 11 | : config.build.env 12 | 13 | var webpackConfig = merge(baseWebpackConfig, { 14 | module: { 15 | loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true }) 16 | }, 17 | devtool: config.build.productionSourceMap ? '#source-map' : false, 18 | output: { 19 | path: config.build.assetsRoot, 20 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 21 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 22 | }, 23 | vue: { 24 | loaders: utils.cssLoaders({ 25 | sourceMap: config.build.productionSourceMap, 26 | extract: true 27 | }) 28 | }, 29 | plugins: [ 30 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 31 | new webpack.DefinePlugin({ 32 | 'process.env': env 33 | }), 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | } 38 | }), 39 | new webpack.optimize.OccurenceOrderPlugin(), 40 | // extract css into its own file 41 | new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')), 42 | // generate dist index.html with correct asset hash for caching. 43 | // you can customize output by editing /index.html 44 | // see https://github.com/ampedandwired/html-webpack-plugin 45 | new HtmlWebpackPlugin({ 46 | filename: process.env.NODE_ENV === 'testing' 47 | ? 'index.html' 48 | : config.build.index, 49 | template: 'index.html', 50 | inject: true, 51 | minify: { 52 | removeComments: true, 53 | collapseWhitespace: true, 54 | removeAttributeQuotes: true 55 | // more options: 56 | // https://github.com/kangax/html-minifier#options-quick-reference 57 | }, 58 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 59 | chunksSortMode: 'dependency' 60 | }), 61 | // split vendor js into its own file 62 | new webpack.optimize.CommonsChunkPlugin({ 63 | name: 'vendor', 64 | minChunks: function (module, count) { 65 | // any required modules inside node_modules are extracted to vendor 66 | return ( 67 | module.resource && 68 | /\.js$/.test(module.resource) && 69 | module.resource.indexOf( 70 | path.join(__dirname, '../node_modules') 71 | ) === 0 72 | ) 73 | } 74 | }), 75 | // extract webpack runtime and module manifest to its own file in order to 76 | // prevent vendor hash from being updated whenever app bundle is updated 77 | new webpack.optimize.CommonsChunkPlugin({ 78 | name: 'manifest', 79 | chunks: ['vendor'] 80 | }) 81 | ] 82 | }) 83 | 84 | if (config.build.productionGzip) { 85 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 86 | 87 | webpackConfig.plugins.push( 88 | new CompressionWebpackPlugin({ 89 | asset: '[path].gz[query]', 90 | algorithm: 'gzip', 91 | test: new RegExp( 92 | '\\.(' + 93 | config.build.productionGzipExtensions.join('|') + 94 | ')$' 95 | ), 96 | threshold: 10240, 97 | minRatio: 0.8 98 | }) 99 | ) 100 | } 101 | 102 | module.exports = webpackConfig 103 | -------------------------------------------------------------------------------- /frontend/config/base.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | API_HOST: JSON.stringify(process.env.API_HOST), 3 | GITHUB_CLIENT_ID: JSON.stringify(process.env.GITHUB_CLIENT_ID), 4 | }; 5 | -------------------------------------------------------------------------------- /frontend/config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var baseEnv = require('./base.env') 3 | 4 | module.exports = merge(baseEnv, { 5 | NODE_ENV: '"development"' 6 | }); 7 | -------------------------------------------------------------------------------- /frontend/config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | require('dotenv').config({path: '../.env'}); 3 | var webpack = require('webpack'); 4 | var path = require('path'); 5 | 6 | 7 | module.exports = { 8 | build: { 9 | env: require('./prod.env'), 10 | index: path.resolve(__dirname, '../dist/index.html'), 11 | assetsRoot: path.resolve(__dirname, '../dist'), 12 | assetsSubDirectory: '', 13 | assetsPublicPath: '/', 14 | productionSourceMap: true, 15 | // Gzip off by default as many popular static hosts such as 16 | // Surge or Netlify already gzip all static assets for you. 17 | // Before setting to `true`, make sure to: 18 | // npm install --save-dev compression-webpack-plugin 19 | productionGzip: false, 20 | productionGzipExtensions: ['js', 'css'] 21 | }, 22 | dev: { 23 | env: require('./dev.env'), 24 | port: 8080, 25 | assetsSubDirectory: '', 26 | assetsPublicPath: '/', 27 | proxyTable: {}, 28 | // CSS Sourcemaps off by default because relative paths are "buggy" 29 | // with this option, according to the CSS-Loader README 30 | // (https://github.com/webpack/css-loader#sourcemaps) 31 | // In our experience, they generally work as expected, 32 | // just be aware of this issue when enabling this option. 33 | cssSourceMap: false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/config/prod.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge'); 2 | var baseEnv = require('./base.env'); 3 | 4 | module.exports = merge(baseEnv, { 5 | NODE_ENV: '"production"' 6 | }); 7 | -------------------------------------------------------------------------------- /frontend/config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge'); 2 | var baseEnv = require('./base.env'); 3 | 4 | module.exports = merge(baseEnv, { 5 | NODE_ENV: '"testing"' 6 | }); 7 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Is Service Up? Monitor the status of your cloud services 7 | 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isserviceup", 3 | "version": "1.0.0", 4 | "description": "Monitor the status of all your cloud services in one page", 5 | "author": "Marco Pazzaglia ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "build": "node build/build.js", 10 | "unit": "karma start test/unit/karma.conf.js --single-run", 11 | "e2e": "node test/e2e/runner.js", 12 | "test": "npm run unit && npm run e2e", 13 | "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" 14 | }, 15 | "dependencies": { 16 | "dotenv": "^2.0.0", 17 | "vue": "^2.0.1", 18 | "vue-router": "^2.0.1" 19 | }, 20 | "devDependencies": { 21 | "autoprefixer": "^6.4.0", 22 | "babel-core": "^6.0.0", 23 | "babel-eslint": "^7.0.0", 24 | "babel-loader": "^6.0.0", 25 | "babel-plugin-transform-runtime": "^6.0.0", 26 | "babel-preset-es2015": "^6.0.0", 27 | "babel-preset-stage-2": "^6.0.0", 28 | "babel-register": "^6.0.0", 29 | "chai": "^3.5.0", 30 | "chalk": "^1.1.3", 31 | "chromedriver": "^2.21.2", 32 | "connect-history-api-fallback": "^1.1.0", 33 | "cross-spawn": "^4.0.2", 34 | "css-loader": "^0.25.0", 35 | "eslint": "^3.7.1", 36 | "eslint-config-airbnb-base": "^8.0.0", 37 | "eslint-friendly-formatter": "^2.0.5", 38 | "eslint-import-resolver-webpack": "^0.6.0", 39 | "eslint-loader": "^1.5.0", 40 | "eslint-plugin-html": "^1.3.0", 41 | "eslint-plugin-import": "^1.16.0", 42 | "eventsource-polyfill": "^0.9.6", 43 | "express": "^4.13.3", 44 | "extract-text-webpack-plugin": "^1.0.1", 45 | "file-loader": "^0.9.0", 46 | "function-bind": "^1.0.2", 47 | "html-webpack-plugin": "^2.8.1", 48 | "http-proxy-middleware": "^0.17.2", 49 | "inject-loader": "^2.0.1", 50 | "isparta-loader": "^2.0.0", 51 | "json-loader": "^0.5.4", 52 | "karma": "^1.3.0", 53 | "karma-coverage": "^1.1.1", 54 | "karma-mocha": "^1.2.0", 55 | "karma-phantomjs-launcher": "^1.0.0", 56 | "karma-sinon-chai": "^1.2.0", 57 | "karma-sourcemap-loader": "^0.3.7", 58 | "karma-spec-reporter": "0.0.26", 59 | "karma-webpack": "^1.7.0", 60 | "lolex": "^1.4.0", 61 | "mocha": "^3.1.0", 62 | "nightwatch": "^0.9.8", 63 | "opn": "^4.0.2", 64 | "ora": "^0.3.0", 65 | "phantomjs-prebuilt": "^2.1.3", 66 | "selenium-server": "2.53.1", 67 | "semver": "^5.3.0", 68 | "shelljs": "^0.7.4", 69 | "sinon": "^1.17.3", 70 | "sinon-chai": "^2.8.0", 71 | "url-loader": "^0.5.7", 72 | "vue-loader": "^9.4.0", 73 | "vue-style-loader": "^1.0.0", 74 | "webpack": "^1.13.2", 75 | "webpack-dev-middleware": "^1.8.3", 76 | "webpack-hot-middleware": "^2.12.2", 77 | "webpack-merge": "^0.14.1" 78 | }, 79 | "engines": { 80 | "node": ">= 4.0.0", 81 | "npm": ">= 3.0.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /frontend/src/api.js: -------------------------------------------------------------------------------- 1 | import * as config from './config'; 2 | import auth from './auth'; 3 | 4 | $.ajaxSetup({ 5 | error: function(jqXHR, exception) { 6 | if (jqXHR.status == 401) { 7 | auth.logout(); 8 | } 9 | } 10 | }); 11 | 12 | function makeAPIRequest(path, options, callback) { 13 | var defaultOptions = { 14 | url: config.API_HOST + path, 15 | type: "GET", 16 | beforeSend: function(xhr){ 17 | var authHeaders = auth.getAuthHeader(); 18 | Object.keys(authHeaders).forEach(function(k, i) { 19 | xhr.setRequestHeader(k, authHeaders[k]); 20 | }); 21 | }, 22 | // data: JSON.stringify({ ... }), 23 | contentType: "application/json; charset=utf-8", 24 | dataType: "json", 25 | success: callback, 26 | }; 27 | 28 | var ajaxOptions = $.extend({}, defaultOptions, options); 29 | return $.ajax(ajaxOptions); 30 | } 31 | 32 | export function getStatus(type, callback) { 33 | var options = { 34 | data: { 35 | type: type.toLowerCase(), 36 | }, 37 | }; 38 | return makeAPIRequest('/status', options, callback); 39 | } 40 | 41 | export function getUserInfo(callback) { 42 | return makeAPIRequest('/user', {}, callback); 43 | } 44 | 45 | export function updateUserInfo(data, callback) { 46 | var options = { 47 | type: 'POST', 48 | data: JSON.stringify(data), 49 | }; 50 | return makeAPIRequest('/user', options, callback); 51 | } 52 | 53 | export function logout(callback) { 54 | return makeAPIRequest('/user/logout', {}, callback); 55 | } 56 | 57 | export function updateFavoriteStatus(status, service_id, callback) { 58 | var options = { 59 | type: 'POST', 60 | data: JSON.stringify({ 61 | status: status, 62 | service_id: service_id, 63 | }), 64 | }; 65 | return makeAPIRequest('/user/favorite', options, callback); 66 | } 67 | -------------------------------------------------------------------------------- /frontend/src/auth.js: -------------------------------------------------------------------------------- 1 | import * as config from './config'; 2 | 3 | export default { 4 | 5 | user: { 6 | authenticated: false, 7 | username: null, 8 | avatar_url: null, 9 | monitored_status: null, 10 | slack_webhook: null, 11 | }, 12 | 13 | login(code) { 14 | localStorage.setItem('auth_token', code); 15 | this.user.authenticated = true; 16 | }, 17 | 18 | logout() { 19 | localStorage.removeItem('auth_token'); 20 | localStorage.removeItem('user'); 21 | this.user.authenticated = false; 22 | this.user.username = null; 23 | this.user.avatar_url = null; 24 | }, 25 | 26 | restoreAuth() { 27 | var jwt = localStorage.getItem('auth_token'); 28 | this.user.authenticated = !!jwt; 29 | if (this.user.authenticated) { 30 | var user = localStorage.getItem('user'); 31 | if (user) { 32 | user = JSON.parse(user); 33 | this.user.username = user.username; 34 | this.user.avatar_url = user.avatar_url; 35 | } 36 | } 37 | }, 38 | 39 | getAuthHeader() { 40 | return { 41 | 'Authorization': 'Bearer ' + localStorage.getItem('auth_token'), 42 | }; 43 | }, 44 | 45 | setUserInfo(data) { 46 | $.each(data, x => { 47 | this.user[x] = data[x]; 48 | }); 49 | localStorage.setItem('user', JSON.stringify(data)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/components/App.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 72 | -------------------------------------------------------------------------------- /frontend/src/components/Service.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 55 | -------------------------------------------------------------------------------- /frontend/src/components/Services.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 196 | -------------------------------------------------------------------------------- /frontend/src/components/Settings.vue: -------------------------------------------------------------------------------- 1 | 72 | 123 | -------------------------------------------------------------------------------- /frontend/src/components/UserMenu.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 64 | -------------------------------------------------------------------------------- /frontend/src/config.js: -------------------------------------------------------------------------------- 1 | export var API_HOST = process.env.API_HOST || "http://localhost:8000"; 2 | export var SERVICES_REFRESH_INTERVAL = 30; 3 | export var GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || console.log('No GITHUB_CLIENT_ID defined'); 4 | export var GITHUB_LOGIN_URL = 'https://github.com/login/oauth/authorize?client_id=' + GITHUB_CLIENT_ID; 5 | 6 | export var STATUS_LIST = [ 7 | 'unavailable', 8 | 'maintenance', 9 | 'ok', 10 | 'minor', 11 | 'major', 12 | 'critical', 13 | ]; 14 | 15 | export var STATUS_DESCRIPTION = { 16 | ok: 'Operational', 17 | minor: 'Minor Outage', 18 | major: 'Major Outage', 19 | critical: 'Critical Outage', 20 | maintenance: 'Maintenance', 21 | unavailable: 'Status Unavailable', 22 | }; 23 | 24 | export var STATUS_COLOR = { 25 | ok: 'green', 26 | minor: 'yellow', 27 | major: 'orange', 28 | critical: 'red', 29 | maintenance: 'blue', 30 | unavailable: 'gray', 31 | }; 32 | 33 | export var STATUS_ICON = { 34 | ok: 'fa-check', 35 | minor: 'fa-minus-square', 36 | major: 'fa-exclamation-triangle', 37 | critical: 'fa-times', 38 | maintenance: 'fa-wrench', 39 | unavailable: 'fa-question', 40 | }; 41 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import App from './components/App'; 4 | import Services from './components/Services'; 5 | import Settings from './components/Settings.vue'; 6 | import auth from './auth'; 7 | 8 | Vue.use(VueRouter); 9 | 10 | /* eslint-disable no-new */ 11 | 12 | auth.restoreAuth(); 13 | 14 | // check if page come from server redirect 15 | var hash = window.location.hash; 16 | if (hash.startsWith('#/?code=')) { 17 | let code = hash.substr('#/?code='.length); 18 | auth.login(code); 19 | // remove code from url for safety purposes 20 | window.location.hash = '#/?login-success'; 21 | } 22 | 23 | var authMiddleware = (to, from, next) => { 24 | if (!auth.user.authenticated) { 25 | next({ path:'/' }); 26 | } else { 27 | next() 28 | } 29 | }; 30 | 31 | var routes = [ 32 | {path: '/', component: Services }, 33 | {path: '/settings', component: Settings, beforeEnter: authMiddleware, }, 34 | ]; 35 | 36 | export var router = new VueRouter({ 37 | routes 38 | }); 39 | 40 | const app = new Vue({ 41 | el: '#app', 42 | router, 43 | render: h => h(App) 44 | }); 45 | -------------------------------------------------------------------------------- /frontend/src/utils/notifications.js: -------------------------------------------------------------------------------- 1 | function isNotificationSupported() { 2 | return ("Notification" in window); 3 | } 4 | 5 | export function requestNotificationPermission() { 6 | if (!isNotificationSupported()) { 7 | return; 8 | } 9 | Notification.requestPermission(); 10 | } 11 | 12 | export function spawnNotification(title, options) { 13 | if (!isNotificationSupported()) { 14 | return; 15 | } 16 | return new Notification(title, options); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /frontend/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/.gitkeep -------------------------------------------------------------------------------- /frontend/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/favicon.ico -------------------------------------------------------------------------------- /frontend/static/images/icons/atlassian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/atlassian.png -------------------------------------------------------------------------------- /frontend/static/images/icons/authorizedotnet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/authorizedotnet.jpg -------------------------------------------------------------------------------- /frontend/static/images/icons/aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/aws.png -------------------------------------------------------------------------------- /frontend/static/images/icons/azure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/azure.png -------------------------------------------------------------------------------- /frontend/static/images/icons/bitbucket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/bitbucket.png -------------------------------------------------------------------------------- /frontend/static/images/icons/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/box.png -------------------------------------------------------------------------------- /frontend/static/images/icons/chargify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/chargify.png -------------------------------------------------------------------------------- /frontend/static/images/icons/circleci.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/circleci.png -------------------------------------------------------------------------------- /frontend/static/images/icons/cloudflare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/cloudflare.png -------------------------------------------------------------------------------- /frontend/static/images/icons/codeclimate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/codeclimate.png -------------------------------------------------------------------------------- /frontend/static/images/icons/codeship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/codeship.png -------------------------------------------------------------------------------- /frontend/static/images/icons/compose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/compose.png -------------------------------------------------------------------------------- /frontend/static/images/icons/datadog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/datadog.png -------------------------------------------------------------------------------- /frontend/static/images/icons/disqus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/disqus.png -------------------------------------------------------------------------------- /frontend/static/images/icons/dnsimple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/dnsimple.png -------------------------------------------------------------------------------- /frontend/static/images/icons/do.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/do.png -------------------------------------------------------------------------------- /frontend/static/images/icons/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/docker.png -------------------------------------------------------------------------------- /frontend/static/images/icons/dropbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/dropbox.png -------------------------------------------------------------------------------- /frontend/static/images/icons/duo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/duo.png -------------------------------------------------------------------------------- /frontend/static/images/icons/dyn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/dyn.png -------------------------------------------------------------------------------- /frontend/static/images/icons/fastly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/fastly.png -------------------------------------------------------------------------------- /frontend/static/images/icons/ftrack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/ftrack.png -------------------------------------------------------------------------------- /frontend/static/images/icons/gcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/gcloud.png -------------------------------------------------------------------------------- /frontend/static/images/icons/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/github.png -------------------------------------------------------------------------------- /frontend/static/images/icons/gitlab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/gitlab.png -------------------------------------------------------------------------------- /frontend/static/images/icons/gocardless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/gocardless.png -------------------------------------------------------------------------------- /frontend/static/images/icons/gotomeeting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/gotomeeting.png -------------------------------------------------------------------------------- /frontend/static/images/icons/harvest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/harvest.png -------------------------------------------------------------------------------- /frontend/static/images/icons/hashicorp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/hashicorp.png -------------------------------------------------------------------------------- /frontend/static/images/icons/heroku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/heroku.png -------------------------------------------------------------------------------- /frontend/static/images/icons/honeybadger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/honeybadger.png -------------------------------------------------------------------------------- /frontend/static/images/icons/linode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/linode.png -------------------------------------------------------------------------------- /frontend/static/images/icons/loggly.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/loggly.jpg -------------------------------------------------------------------------------- /frontend/static/images/icons/mailgun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/mailgun.png -------------------------------------------------------------------------------- /frontend/static/images/icons/maxcdn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/maxcdn.png -------------------------------------------------------------------------------- /frontend/static/images/icons/newrelic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/newrelic.png -------------------------------------------------------------------------------- /frontend/static/images/icons/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/npm.png -------------------------------------------------------------------------------- /frontend/static/images/icons/opbeat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/opbeat.png -------------------------------------------------------------------------------- /frontend/static/images/icons/packagecloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/packagecloud.png -------------------------------------------------------------------------------- /frontend/static/images/icons/pagerduty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/pagerduty.png -------------------------------------------------------------------------------- /frontend/static/images/icons/pingdom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/pingdom.png -------------------------------------------------------------------------------- /frontend/static/images/icons/pingidentity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/pingidentity.png -------------------------------------------------------------------------------- /frontend/static/images/icons/pusher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/pusher.png -------------------------------------------------------------------------------- /frontend/static/images/icons/pyinfra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/pyinfra.png -------------------------------------------------------------------------------- /frontend/static/images/icons/quay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/quay.png -------------------------------------------------------------------------------- /frontend/static/images/icons/redislabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/redislabs.png -------------------------------------------------------------------------------- /frontend/static/images/icons/rollbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/rollbar.png -------------------------------------------------------------------------------- /frontend/static/images/icons/rubygems.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/rubygems.png -------------------------------------------------------------------------------- /frontend/static/images/icons/sendgrid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/sendgrid.png -------------------------------------------------------------------------------- /frontend/static/images/icons/sendwithus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/sendwithus.png -------------------------------------------------------------------------------- /frontend/static/images/icons/sentry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/sentry.png -------------------------------------------------------------------------------- /frontend/static/images/icons/shotgun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/shotgun.png -------------------------------------------------------------------------------- /frontend/static/images/icons/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/slack.png -------------------------------------------------------------------------------- /frontend/static/images/icons/sparkpost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/sparkpost.png -------------------------------------------------------------------------------- /frontend/static/images/icons/statusio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/statusio.png -------------------------------------------------------------------------------- /frontend/static/images/icons/statuspage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/statuspage.png -------------------------------------------------------------------------------- /frontend/static/images/icons/stormpath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/stormpath.png -------------------------------------------------------------------------------- /frontend/static/images/icons/stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/stripe.png -------------------------------------------------------------------------------- /frontend/static/images/icons/travis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/travis.png -------------------------------------------------------------------------------- /frontend/static/images/icons/twilio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/twilio.png -------------------------------------------------------------------------------- /frontend/static/images/icons/victorops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/victorops.png -------------------------------------------------------------------------------- /frontend/static/images/icons/vultr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/vultr.png -------------------------------------------------------------------------------- /frontend/static/images/icons/weblate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/icons/weblate.png -------------------------------------------------------------------------------- /frontend/static/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/loading.gif -------------------------------------------------------------------------------- /frontend/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/frontend/static/images/logo.png -------------------------------------------------------------------------------- /frontend/static/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background-color: white; 3 | } 4 | 5 | .container { 6 | width: 90%; 7 | max-width: 850px; 8 | font-size: 1.2em; 9 | } 10 | 11 | #header { 12 | margin-top: 3em; 13 | margin-bottom: 3em; 14 | height: 60px; 15 | } 16 | 17 | #header .row, #header .col-lg-12 { 18 | height: 100%; 19 | } 20 | 21 | #header .navigation { 22 | float: right; 23 | display: flex; 24 | justify-content: flex-end; 25 | align-items: flex-end; 26 | height: 100%; 27 | padding-bottom: 5px; 28 | } 29 | 30 | #header .menu { 31 | } 32 | 33 | .dropdown-menu { 34 | right: 0; 35 | left: inherit; 36 | } 37 | 38 | .dropdown-signout { 39 | width: 100%; 40 | text-align: left; 41 | background: none; 42 | border: 0; 43 | } 44 | 45 | @media only screen and (max-width : 768px) { 46 | .container { 47 | font-size: 1em; 48 | } 49 | } 50 | 51 | .loading { 52 | text-align: center; 53 | padding: 30px; 54 | } 55 | .loading img { 56 | margin-bottom: 10px; 57 | } 58 | 59 | .footer { 60 | margin-top: 3em; 61 | margin-bottom: 3em; 62 | color: gray; 63 | font-size: 0.8em; 64 | text-align: center; 65 | } 66 | 67 | .footer a { 68 | color: gray; 69 | } 70 | 71 | .logo { 72 | float: left; 73 | } 74 | 75 | .logo-img { 76 | max-width: 100%; 77 | } 78 | 79 | .last-update { 80 | text-align: right; 81 | color: gray; 82 | font-size: 0.8em; 83 | } 84 | 85 | .services-container { 86 | border-radius: 3px; 87 | border: 1px solid #eee; 88 | margin-bottom: 20px; 89 | cursor: pointer; 90 | } 91 | 92 | .service-row { 93 | display: flex; 94 | align-items: baseline; 95 | border: 0px solid #eee; 96 | border-bottom-width: 1px; 97 | } 98 | 99 | .service-row.last-element { 100 | border-bottom-width: 0px; 101 | } 102 | 103 | .service-star { 104 | color: #b1b32b; 105 | width: 53px; 106 | text-align: center; 107 | border-right: 1px solid #eee; 108 | padding-bottom: 20px; 109 | padding-top: 20px; 110 | } 111 | .service-star:hover { 112 | background-color: #f3f3f3; 113 | } 114 | 115 | #services .nav-tabs { 116 | border-bottom: 0; 117 | } 118 | 119 | #services .empty-list { 120 | padding: 20px; 121 | border: 1px solid #eee; 122 | } 123 | 124 | .service-container { 125 | padding: 20px; 126 | width: 100%; 127 | } 128 | .service-container:hover { 129 | background-color: #f3f3f3; 130 | } 131 | 132 | .service-name { 133 | margin-left: 5px; 134 | } 135 | 136 | .service-status { 137 | float:right; 138 | margin-right: 10px; 139 | } 140 | 141 | .service-container .icon-indicator { 142 | float: right; 143 | position: relative; 144 | top: 4px; 145 | right: 0; 146 | } 147 | 148 | .service-icon { 149 | vertical-align: middle; 150 | max-height: 15px; 151 | max-width: 15px; 152 | position: relative; 153 | bottom: 1px; 154 | } 155 | 156 | .status-green .icon-indicator, .status-green a, .status-green .status-description { 157 | color: green; 158 | } 159 | 160 | .status-red .icon-indicator, .status-red a, .status-red .status-description { 161 | color: #f43f20; 162 | } 163 | 164 | .status-blue .icon-indicator, .status-blue a, .status-blue .status-description { 165 | color: #3498DB; 166 | } 167 | 168 | .status-orange .icon-indicator, .status-orange a, .status-orange .status-description { 169 | color: #f18500; 170 | } 171 | 172 | .status-yellow .icon-indicator, .status-yellow a, .status-yellow .status-description { 173 | color: #f1c40f; 174 | } 175 | 176 | .status-gray .icon-indicator, .status-gray a, .status-gray .status-description { 177 | color: gray; 178 | } 179 | 180 | .clickable { 181 | cursor: pointer; 182 | } 183 | 184 | .legend { 185 | margin-top: 20px; 186 | } 187 | 188 | .legend-list { 189 | list-style: none; 190 | padding: 0; 191 | margin: 0; 192 | font-size: 0.8em; 193 | } 194 | 195 | .status-description { 196 | } 197 | 198 | .legend-list .icon-indicator { 199 | width: 20px; 200 | height: 10px; 201 | } 202 | 203 | .legend-list .status-description { 204 | } 205 | 206 | .hidden-xxs { 207 | display: block !important; 208 | } 209 | 210 | @media only screen and (max-width : 450px) { 211 | .hidden-xxs { 212 | display: none !important; 213 | } 214 | } 215 | 216 | .visible-xxs { 217 | display: none; 218 | } 219 | 220 | @media only screen and (max-width : 450px) { 221 | .visible-xxs { 222 | display: block; 223 | } 224 | } 225 | 226 | #user-menu-dropdown:hover, #user-menu-dropdown:focus { 227 | text-decoration: none; 228 | } 229 | 230 | .dropdown-caret { 231 | display: inline-block; 232 | width: 0; 233 | height: 0; 234 | vertical-align: middle; 235 | content: ""; 236 | border: 6px solid; 237 | border-right-color: transparent; 238 | border-bottom-color: transparent; 239 | border-left-color: transparent; 240 | position: relative; 241 | top: 5px; 242 | } 243 | 244 | .boxed-group { 245 | position: relative; 246 | margin-bottom: 30px; 247 | border-radius: 3px; 248 | } 249 | 250 | /*.boxed-group input[type=checkbox] {*/ 251 | /*padding: 0;*/ 252 | /*margin: 0;*/ 253 | /*}*/ 254 | 255 | .boxed-group>h3, .boxed-group .heading { 256 | display: block; 257 | padding: 9px 10px 10px; 258 | margin: 0; 259 | font-size: 14px; 260 | line-height: 17px; 261 | background-color: #f5f5f5; 262 | border: 1px solid #d8d8d8; 263 | border-bottom: 0; 264 | border-radius: 3px 3px 0 0; 265 | } 266 | 267 | .boxed-group-inner { 268 | padding: 10px; 269 | color: #666; 270 | background: #fff; 271 | border: 1px solid #d8d8d8; 272 | border-bottom-right-radius: 3px; 273 | border-bottom-left-radius: 3px; 274 | } 275 | 276 | .note { 277 | display: block; 278 | margin: 0; 279 | font-size: 12px; 280 | font-weight: normal; 281 | color: #666; 282 | } 283 | -------------------------------------------------------------------------------- /isserviceup/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/isserviceup/__init__.py -------------------------------------------------------------------------------- /isserviceup/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/isserviceup/api/__init__.py -------------------------------------------------------------------------------- /isserviceup/api/auth.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from flask import redirect 3 | from flask import request 4 | 5 | from isserviceup.config import config 6 | from isserviceup.helpers import github 7 | from isserviceup.storage import sessions 8 | from isserviceup.storage.users import upsert_user 9 | 10 | mod = Blueprint('auth', __name__) 11 | 12 | 13 | @mod.route('/oauth_callback', methods=['GET']) 14 | def oauth_callback(): 15 | code = request.args['code'] 16 | 17 | res = github.get_access_token(code) 18 | access_token = res.json()['access_token'] 19 | 20 | res = github.get_user_info(access_token) 21 | data = res.json() 22 | 23 | user = upsert_user(github_access_token=access_token, 24 | avatar_url=data['avatar_url'], 25 | username=data['login']) 26 | print('user={}'.format(user)) 27 | 28 | sid = sessions.create({ 29 | 'user_id': str(user.id), 30 | }) 31 | 32 | return redirect(config.FRONTEND_URL + '#/?code=' + sid) 33 | -------------------------------------------------------------------------------- /isserviceup/api/status.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify 2 | from flask import request 3 | 4 | from isserviceup import managers 5 | from isserviceup.helpers.decorators import authenticated 6 | from isserviceup.services import SERVICES 7 | from isserviceup.services.models.service import Status 8 | from isserviceup.storage.favorites import get_favorite_services 9 | from isserviceup.storage.services import get_status as get_services_status 10 | 11 | mod = Blueprint('status', __name__) 12 | 13 | 14 | @mod.route('', methods=['GET']) 15 | @authenticated(blocking=False) 16 | def status(user): 17 | type = request.args.get('type') 18 | favorite_services = [] 19 | 20 | services = SERVICES.values() 21 | 22 | if user: 23 | favorite_services = get_favorite_services(str(user.id)) 24 | 25 | if type == 'favorite': 26 | services = favorite_services 27 | for service in services: 28 | service.star = True 29 | else: 30 | favorite_services = {x.name: True for x in favorite_services} 31 | 32 | values = get_services_status(managers.rclient, services) 33 | 34 | data = [] 35 | for i, service in enumerate(services): 36 | status = values[i] if values[i] else Status.unavailable 37 | star = True if type == 'favorite' else (service.name in favorite_services) 38 | s = { 39 | 'name': service.name, 40 | 'icon_url': service.icon_url, 41 | 'status_url': service.status_url, 42 | 'status': status.name, 43 | 'star': star, 44 | 'id': service.id, 45 | } 46 | data.append(s) 47 | 48 | return jsonify({ 49 | 'data': { 50 | 'services': data, 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /isserviceup/api/user.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify 2 | from flask import request 3 | 4 | from isserviceup.helpers.decorators import authenticated 5 | from isserviceup.helpers.exceptions import ApiException 6 | from isserviceup.services import SERVICES 7 | from isserviceup.storage import sessions 8 | from isserviceup.storage.favorites import update_favorite_status 9 | 10 | mod = Blueprint('user', __name__) 11 | 12 | 13 | @mod.route('', methods=['GET']) 14 | @authenticated() 15 | def get_user(user): 16 | return jsonify(user.as_dict()) 17 | 18 | 19 | @mod.route('', methods=['POST']) 20 | @authenticated() 21 | def edit_user(user): 22 | data = request.json 23 | try: 24 | user.edit(data) 25 | except ValueError: 26 | raise ApiException('bad request', 400) 27 | return jsonify(user.as_dict()) 28 | 29 | 30 | @mod.route('/logout', methods=['GET']) 31 | @authenticated(blocking=False) 32 | def logout(user): 33 | if user: 34 | sessions.destroy(user.sid) 35 | return jsonify({ 36 | 'status': 'ok', 37 | }) 38 | 39 | 40 | @mod.route('/favorite', methods=['POST']) 41 | @authenticated() 42 | def star(user): 43 | data = request.json 44 | 45 | if not data or data.get('service_id') is None or data.get('status') is None: 46 | raise ApiException('bad request', 400) 47 | 48 | service_id = data['service_id'] 49 | status = data['status'] 50 | 51 | try: 52 | SERVICES[service_id] 53 | except Exception: 54 | raise ApiException('bad request', 400) 55 | 56 | update_favorite_status(str(user.id), service_id, status) 57 | 58 | return jsonify({ 59 | 'status': 'ok', 60 | }) 61 | -------------------------------------------------------------------------------- /isserviceup/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import traceback 3 | 4 | from flask import Flask, jsonify 5 | from flask_cors import CORS 6 | from raven.contrib.flask import Sentry 7 | 8 | from isserviceup.api import auth as auth_bp 9 | from isserviceup.api import status as status_bp 10 | from isserviceup.api import user as user_bp 11 | from isserviceup.config import config 12 | from isserviceup.helpers import exceptions 13 | from isserviceup.helpers.exceptions import ApiException 14 | 15 | static_url_path = '' if config.SERVE_STATIC_FILES else None 16 | static_folder = '../frontend/dist' if config.SERVE_STATIC_FILES else None 17 | app = Flask(__name__, static_url_path=static_url_path, static_folder=static_folder) 18 | app.config.from_object(config) 19 | app.debug = config.DEBUG 20 | CORS(app) 21 | 22 | sentry = None 23 | if config.SENTRY_DSN: 24 | sentry = Sentry(app, logging=True, level=logging.ERROR) 25 | 26 | 27 | @app.errorhandler(Exception) 28 | def handle_generic_exception(error): 29 | print('Exception={}'.format(error)) 30 | traceback.print_exc() 31 | 32 | if sentry: 33 | sentry.captureException() 34 | 35 | return exceptions.handle_exception(error) 36 | 37 | 38 | app.register_blueprint(status_bp.mod, url_prefix='/status') 39 | app.register_blueprint(auth_bp.mod, url_prefix='/auth') 40 | app.register_blueprint(user_bp.mod, url_prefix='/user') 41 | 42 | 43 | @app.route('/', methods=['GET']) 44 | def index(): 45 | if config.SERVE_STATIC_FILES: 46 | return app.send_static_file('index.html') 47 | else: 48 | raise ApiException('page not found', 404) 49 | 50 | 51 | if __name__ == '__main__': 52 | app.run(host='0.0.0.0', port=8000) 53 | -------------------------------------------------------------------------------- /isserviceup/celeryapp.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | import raven 5 | from celery import Celery 6 | from celery.utils.log import get_task_logger 7 | from raven.conf import setup_logging 8 | from raven.contrib.celery import register_signal, register_logger_signal 9 | from raven.handlers.logging import SentryHandler 10 | 11 | from isserviceup import managers 12 | from isserviceup.config import celery as celeryconfig 13 | from isserviceup.config import config 14 | from isserviceup.models.favorite import Favorite 15 | from isserviceup.notifiers.slack import Slack 16 | from isserviceup.services import SERVICES 17 | from isserviceup.services.models.service import Status 18 | from isserviceup.storage.services import set_service_status, set_last_update 19 | 20 | MAX_RETRIES = 3 21 | DELAY_RETRY = 2 22 | 23 | _notified_on_startup = {} 24 | 25 | app = Celery('app') 26 | app.config_from_object(celeryconfig) 27 | 28 | logger = get_task_logger(__name__) 29 | 30 | if config.SENTRY_DSN: 31 | client = raven.Client(config.SENTRY_DSN) 32 | register_logger_signal(client, loglevel=logging.ERROR) 33 | register_signal(client) 34 | # report logging errors 35 | handler = SentryHandler(client) 36 | setup_logging(handler) 37 | # show sentry errors in the console 38 | logger = logging.getLogger('sentry.errors') 39 | logger.setLevel(logging.ERROR) 40 | logger.addHandler(logging.StreamHandler()) 41 | 42 | 43 | @app.task(name='update-services-status') 44 | def update_services_status(): 45 | set_last_update(managers.rclient, time.time()) 46 | for service_id in SERVICES: 47 | update_service_status.delay(service_id) 48 | 49 | 50 | @app.task(bind=True, max_retries=MAX_RETRIES) 51 | def update_service_status(self, service_id): 52 | service = SERVICES[service_id] 53 | logger.info('Updating status for service {}'.format(service.name)) 54 | try: 55 | status = service.get_status() 56 | except Exception as exc: 57 | if self.request.retries == MAX_RETRIES-1: # last retry 58 | set_service_status(managers.rclient, service, Status.unavailable) 59 | raise 60 | else: 61 | return self.retry(exc=exc, countdown=DELAY_RETRY) 62 | 63 | logger.info('Service={} has status={}'.format(service.name, status.name)) 64 | old_status = set_service_status(managers.rclient, service, status) 65 | if ((config.NOTIFY_ON_STARTUP and service.id not in _notified_on_startup) 66 | or (old_status is not None and old_status != status)): 67 | broadcast_status_change.delay( 68 | service.id, old_status.name if old_status else "", status.name) 69 | _notified_on_startup[service.id] = True 70 | 71 | 72 | @app.task() 73 | def broadcast_status_change(service_id, old_status, new_status): 74 | send_all_slack_notifications.delay(service_id, old_status, new_status) 75 | 76 | for i in range(len(config.NOTIFIERS)): 77 | notify_status_change.delay(i, service_id, old_status, new_status) 78 | 79 | 80 | @app.task() 81 | def notify_status_change(idx, service_id, old_status, new_status): 82 | service = SERVICES[service_id] 83 | notifier = config.NOTIFIERS[idx] 84 | notifier.notify(service, old_status, new_status) 85 | 86 | 87 | @app.task() 88 | def send_all_slack_notifications(service_id, old_status, new_status): 89 | favs = Favorite.objects(service_id=service_id, 90 | slack_webhook__ne=None, 91 | monitored_status__all=[old_status, new_status]) 92 | if not favs: 93 | return 94 | for fav in favs: 95 | send_slack_notification.delay(fav.slack_webhook, service_id, old_status, new_status) 96 | 97 | 98 | @app.task() 99 | def send_slack_notification(webhook_url, service_id, old_status, new_status): 100 | service = SERVICES[service_id] 101 | desc = config.get_status_description() 102 | old_status_desc = desc[Status[old_status]] 103 | new_status_desc = desc[Status[new_status]] 104 | Slack(webhook_url).notify(service, old_status_desc, new_status_desc) 105 | # TODO: remove webhook if the request fails X times in a row 106 | 107 | 108 | if __name__ == '__main__': 109 | app.start() 110 | -------------------------------------------------------------------------------- /isserviceup/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/isserviceup/config/__init__.py -------------------------------------------------------------------------------- /isserviceup/config/celery.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from isserviceup.config import config 4 | 5 | BROKER_URL = config.CELERY_BROKER 6 | BROKER_HEARTBEAT = 10 7 | 8 | CELERY_RESULT_BACKEND = config.CELERY_BACKEND 9 | CELERY_TASK_RESULT_EXPIRES = 1 * 3600 10 | CELERY_RESULT_SERIALIZER = 'json' 11 | CELERY_TASK_SERIALIZER = 'json' 12 | CELERY_ACCEPT_CONTENT = ['json'] 13 | CELERY_ALWAYS_EAGER = config.CELERY_EAGER 14 | CELERY_EAGER_PROPAGATES_EXCEPTIONS = True 15 | CELERY_ACKS_LATE = False 16 | CELERY_ANNOTATIONS = {'*': {'max_retries': 10, 'default_retry_delay': 60}} 17 | CELERYD_PREFETCH_MULTIPLIER = 1 18 | CELERYD_TASK_SOFT_TIME_LIMIT = 600 19 | CELERYD_TASK_TIME_LIMIT = 1800 20 | CELERY_ENABLE_UTC = True 21 | CELERY_IGNORE_RESULT = True 22 | 23 | CELERYBEAT_SCHEDULE = { 24 | 'update-status': { 25 | 'task': 'update-services-status', 26 | 'schedule': timedelta(seconds=config.STATUS_UPDATE_INTERVAL), 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /isserviceup/config/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import stat 3 | import shutil 4 | from decouple import config 5 | from isserviceup.notifiers.cachet import Cachet 6 | 7 | 8 | def s2l(x): 9 | return [y.strip() for y in x.split(',')] if x else None 10 | 11 | 12 | DEBUG = config('DEBUG', cast=bool, default=False) 13 | FRONTEND_URL = config('FRONTEND_URL', default='http://localhost:8000/') 14 | REDIS_URL = config('REDIS_URL', default='redis://redis:devpassword@redis') 15 | MONGO_URL = config('MONGO_URL', default='mongodb://mongo/isserviceup') 16 | STATUS_UPDATE_INTERVAL = config('STATUS_UPDATE_INTERVAL', cast=int, default=30) 17 | 18 | SERVE_STATIC_FILES = config('SERVE_STATIC_FILES', cast=bool, default=True) 19 | 20 | SENTRY_DSN = config('SENTRY_DSN', default=None) 21 | 22 | CELERY_EAGER = config('CELERY_EAGER', cast=bool, default=False) 23 | CELERY_BROKER = config('CELERY_BROKER', default=REDIS_URL) 24 | CELERY_BACKEND = config('CELERY_BACKEND', default=REDIS_URL) 25 | 26 | GITHUB_CLIENT_ID = config('GITHUB_CLIENT_ID', default=None) 27 | GITHUB_CLIENT_SECRET = config('GITHUB_CLIENT_SECRET', default=None) 28 | 29 | SLACK_WEB_HOOK_URL = config('SLACK_WEB_HOOK_URL', default=None) 30 | 31 | NOTIFIERS = [ 32 | # Slack(SLACK_WEB_HOOK_URL) 33 | ] 34 | # If True, on the first status update of the run, it will notify the status 35 | # even if they didn't change. 36 | NOTIFY_ON_STARTUP = config('NOTIFY_ON_STARTUP', default=False, cast=bool) 37 | 38 | # To use, set CACHET_NOTIFIER=True, and set the values for 39 | # CACHET_URL, CACHET_TOKEN, CACHET_COMPONENTS. 40 | CACHET_NOTIFIER = config('CACHET_NOTIFIER', default=False, cast=bool) 41 | if CACHET_NOTIFIER: 42 | NOTIFIERS.append(Cachet()) 43 | 44 | # List of services separated by comma, a service is represented by the name of 45 | # its class. If not specified the server will fetch the status of all services 46 | # inside the services folder. 47 | SERVICES = config('SERVICES', cast=s2l, default=None) 48 | 49 | # This copies the key from a location set in the PRIVATE_SSH_KEY option to the 50 | # ~/.ssh directory. For example, you can share a key in the 'shared' directory 51 | # and then set the option to '/home/app/shared/keyfilename' so services that 52 | # need to connect through SSH can use it. If you are going to share it, the 53 | # file needs to have permissions to be read by other users to be able to copy 54 | # it. Also, remember is a PRIVATE key, so generate a new one, and give it 55 | # the minimum pemissions needed to check if the service is up and running. 56 | PRIVATE_SSH_KEY = config('PRIVATE_SSH_KEY', default='') 57 | 58 | 59 | # When you need to use the SSH key, call ensure_private_ssh_key that will 60 | # return if the key was set and properly copied in the corresponding directory. 61 | def ensure_private_ssh_key(): 62 | global PRIVATE_SSH_KEY 63 | if PRIVATE_SSH_KEY: 64 | key_path = os.path.expanduser('~/.ssh') 65 | key_file = os.path.join(key_path, 'id_rsa') 66 | try: 67 | if not os.path.isdir(key_path): 68 | os.mkdir(key_path) 69 | if not os.path.isfile(key_file): 70 | shutil.copyfile(PRIVATE_SSH_KEY, key_file) 71 | os.chmod(key_file, stat.S_IWRITE | stat.S_IREAD) 72 | except (IOError, OSError) as error: 73 | PRIVATE_SSH_KEY = '' 74 | print(error) 75 | else: 76 | return True 77 | return False 78 | 79 | 80 | # TODO: unify with frontend config 81 | def get_status_description(): 82 | from isserviceup.services.models.service import Status 83 | return { 84 | Status.ok: 'Operational', 85 | Status.minor: 'Minor Outage', 86 | Status.major: 'Major Outage', 87 | Status.critical: 'Critical Outage', 88 | Status.maintenance: 'Maintenance', 89 | Status.unavailable: 'Status Unavailable', 90 | } 91 | -------------------------------------------------------------------------------- /isserviceup/config/gunicorn.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from multiprocessing import cpu_count 3 | 4 | 5 | def max_workers(): 6 | return cpu_count() 7 | 8 | 9 | bind = '0.0.0.0:8000' 10 | accesslog = '-' 11 | access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" in %(D)sµs' 12 | 13 | max_requests = 1000 14 | worker_class = 'eventlet' 15 | 16 | workers = max_workers() 17 | -------------------------------------------------------------------------------- /isserviceup/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/isserviceup/helpers/__init__.py -------------------------------------------------------------------------------- /isserviceup/helpers/decorators.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from six import wraps 3 | 4 | from isserviceup.helpers.exceptions import ApiException 5 | from isserviceup.storage import sessions 6 | from isserviceup.storage.users import get_user 7 | 8 | 9 | def authenticated(blocking=True): 10 | 11 | def decorator(f): 12 | @wraps(f) 13 | def wrapper(*args, **kwargs): 14 | user = None 15 | auth = request.headers.get('Authorization') 16 | if auth and len(auth) >= 7: 17 | sid = auth[7:] # 'Bearer ' prefix 18 | session = sessions.get(sid) 19 | if session and session.get('user_id'): 20 | user = get_user(session['user_id']) 21 | if user: 22 | user.sid = sid 23 | if blocking and not user: 24 | raise ApiException('unauthorized', status_code=401) 25 | res = f(user=user, *args, **kwargs) 26 | return res 27 | return wrapper 28 | 29 | return decorator 30 | -------------------------------------------------------------------------------- /isserviceup/helpers/exceptions.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from werkzeug.exceptions import HTTPException 3 | 4 | 5 | class ApiException(Exception): 6 | def __init__(self, message, status_code=400, **kwargs): 7 | super(ApiException, self).__init__() 8 | self.message = message 9 | self.status_code = status_code 10 | self.extra = kwargs 11 | 12 | 13 | def format_exception(message, code=None, extra=None): 14 | res = { 15 | 'status': 'error', 16 | 'error': message or 'server error', 17 | 'code': code, 18 | } 19 | if extra is not None: 20 | res.update(extra) 21 | return res 22 | 23 | 24 | def handle_exception(error): 25 | code = 500 26 | message = None 27 | if hasattr(error, 'status_code') : 28 | code = error.status_code 29 | if hasattr(error, 'message') : 30 | message = str(error.message) 31 | if isinstance(error, HTTPException): 32 | code = error.code 33 | message = str(error) 34 | 35 | extra = error.extra if hasattr(error, 'extra') else None 36 | 37 | response = jsonify(format_exception(message, code=code, extra=extra)) 38 | response.status_code = code 39 | return response 40 | -------------------------------------------------------------------------------- /isserviceup/helpers/github.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from isserviceup.config import config 3 | 4 | 5 | def get_access_token(code): 6 | params = { 7 | 'client_id': config.GITHUB_CLIENT_ID, 8 | 'client_secret': config.GITHUB_CLIENT_SECRET, 9 | 'code': code, 10 | } 11 | headers = { 12 | 'Accept': 'application/json', 13 | } 14 | result = requests.post('https://github.com/login/oauth/access_token', params=params, headers=headers) 15 | return result 16 | 17 | 18 | def get_user_info(access_token): 19 | params = { 20 | 'access_token': access_token, 21 | } 22 | data = requests.get('https://api.github.com/user', params=params) 23 | return data 24 | -------------------------------------------------------------------------------- /isserviceup/helpers/utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | 5 | def random_string(n=10): 6 | return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(n)) 7 | -------------------------------------------------------------------------------- /isserviceup/managers.py: -------------------------------------------------------------------------------- 1 | import redis 2 | from mongoengine import connect 3 | 4 | from isserviceup.config import config 5 | 6 | rclient = redis.from_url(config.REDIS_URL, charset="utf-8", decode_responses=True) 7 | 8 | connect(config.MONGO_URL.split('/')[-1], host=config.MONGO_URL) 9 | -------------------------------------------------------------------------------- /isserviceup/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/isserviceup/models/__init__.py -------------------------------------------------------------------------------- /isserviceup/models/favorite.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from mongoengine import Document, StringField, DateTimeField, ValidationError, ListField, URLField 3 | 4 | 5 | class Favorite(Document): 6 | meta = { 7 | 'indexes': [ 8 | {'fields': ('user_id',) }, 9 | {'fields': ('service_id',) }, 10 | {'fields': ('user_id', 'service_id'), 'unique': True} 11 | ] 12 | } 13 | 14 | user_id = StringField(required=True) 15 | service_id = StringField(required=True) 16 | timestamp = DateTimeField(default=datetime.datetime.now) 17 | 18 | monitored_status = ListField(default=None) 19 | slack_webhook = URLField(default=None) 20 | 21 | -------------------------------------------------------------------------------- /isserviceup/models/user.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from mongoengine import StringField, Document, DateTimeField, BooleanField, ListField, URLField 3 | 4 | from isserviceup.models.favorite import Favorite 5 | from isserviceup.services.models.service import Status 6 | 7 | DEFAULT_MONITORED_STATUS = [ 8 | Status.ok.name, 9 | Status.major.name, 10 | Status.critical.name 11 | ] 12 | 13 | 14 | class User(Document): 15 | username = StringField(required=True, unique=True) 16 | avatar_url = StringField() 17 | github_access_token = StringField() 18 | timestamp = DateTimeField(default=datetime.datetime.now) 19 | 20 | monitored_status = ListField(StringField(), default=DEFAULT_MONITORED_STATUS) 21 | slack_webhook = URLField(default=None) 22 | 23 | def update_favs(self, field, value): 24 | Favorite.objects(user_id=str(self.id))\ 25 | .update(**{'set__{}'.format(field): value}) 26 | 27 | def edit(self, data): 28 | value = data.get('monitored_status') 29 | if value is not None: 30 | if not self.validate_monitored_status(value): 31 | raise ValueError('invalid value for monitored_status') 32 | self.update_favs('monitored_status', value) 33 | self.monitored_status = value 34 | 35 | value = data.get('slack_webhook') 36 | if value is not None: 37 | if value and not value.startswith('https://hooks.slack.com/services/'): 38 | raise ValueError('invalid value for slack_webhook') 39 | if not value: 40 | value = None 41 | self.update_favs('slack_webhook', value) 42 | self.slack_webhook = value 43 | 44 | self.save() 45 | 46 | @staticmethod 47 | def validate_monitored_status(l): 48 | if not isinstance(l, list): 49 | return False 50 | for key in l: 51 | if key not in Status.__members__: 52 | return False 53 | return True 54 | 55 | 56 | def as_dict(self): 57 | return { 58 | 'username': self.username, 59 | 'avatar_url': self.avatar_url, 60 | 'monitored_status': self.monitored_status, 61 | 'slack_webhook': self.slack_webhook, 62 | } 63 | -------------------------------------------------------------------------------- /isserviceup/notifiers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/isserviceup/notifiers/__init__.py -------------------------------------------------------------------------------- /isserviceup/notifiers/cachet.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from enum import Enum 3 | from decouple import config 4 | from isserviceup.notifiers.notifier import Notifier 5 | 6 | 7 | class CachetStatus(Enum): 8 | ok = 1 9 | minor = 2 10 | major = 3 11 | critical = 4 12 | 13 | @staticmethod 14 | def get_cachet_status(status): 15 | from isserviceup.services.models.service import Status 16 | status_map = { 17 | Status.ok: CachetStatus.ok, 18 | Status.maintenance: CachetStatus.minor, 19 | Status.minor: CachetStatus.minor, 20 | Status.major: CachetStatus.major, 21 | Status.critical: CachetStatus.critical, 22 | Status.unavailable: CachetStatus.critical, 23 | } 24 | status = status and Status[status] 25 | return (status_map[status].value if status in status_map 26 | else CachetStatus.critical.value) 27 | 28 | 29 | class Cachet(Notifier): 30 | """Notifies to the Cachet status page components, about the state of their 31 | corresponding services. 32 | 33 | :param cachet_url: The URL for the Cachet status page. 34 | If empty, will get it from config('CACHET_URL'). 35 | :type cachet: str 36 | :param cachet_token: The Cachet API Token. 37 | If empty, will get it from config('CACHET_TOKEN'). 38 | :type cachet_token: str 39 | :param cachet_components: Which components in Cachet are going to be 40 | notified. Is a dictionary with each key being the service class, and 41 | its value, the Cachet component id. If empty, will get it from 42 | config('CACHET_COMPONENTS'), and it has to be a comma separated string 43 | with each service class, a colon, and the component id. (For example, 44 | CACHET_COMPONENTS=Docker:1,Github:4) 45 | :type cachet_components: dict 46 | """ 47 | 48 | def __init__(self, cachet_url="", cachet_token="", 49 | cachet_components=None): 50 | self.cachet_url = cachet_url or config('CACHET_URL', default=None) 51 | self.cachet_token = (cachet_token or 52 | config('CACHET_TOKEN', default=None)) 53 | self.cachet_components = (cachet_components or 54 | config('CACHET_COMPONENTS', default=None, 55 | cast=self._components_to_dict)) 56 | 57 | def _components_to_dict(self, components=None): 58 | try: 59 | return dict([[side.strip() for side in component.split(":")] 60 | for component in components.split(",")]) 61 | except ValueError as error: 62 | print("Value error: {msg}".format(msg=error.message)) 63 | return {} 64 | 65 | def _get_component_name(self, service): 66 | return type(service).__name__ 67 | 68 | def _get_component_url(self, service): 69 | component = self._get_component_name(service) 70 | if component in self.cachet_components: 71 | url = "{base_url}/api/v1/components/{component}".format( 72 | base_url=self.cachet_url.strip("/"), 73 | component=self.cachet_components[component] 74 | ) 75 | return url 76 | return False 77 | 78 | def _get_headers(self): 79 | return { 80 | 'Content-Type': 'application/json', 81 | 'X-Cachet-Token': self.cachet_token, 82 | } 83 | 84 | def _build_payload(self, service, old_status, new_status): 85 | payload = { 86 | 'name': service.name, 87 | 'status': CachetStatus.get_cachet_status(new_status), 88 | 'old_status': CachetStatus.get_cachet_status(old_status), 89 | } 90 | return payload 91 | 92 | def _is_valid(self, service): 93 | return ( 94 | self.cachet_url and self.cachet_token and self.cachet_components 95 | and self._get_component_name(service) in self.cachet_components 96 | ) 97 | 98 | def notify(self, service, old_status, new_status): 99 | if self._is_valid(service): 100 | url = self._get_component_url(service) 101 | headers = self._get_headers() 102 | payload = self._build_payload(service, old_status, new_status) 103 | print("Notifying {} with {}".format(url, payload)) 104 | requests.put(url, json=payload, headers=headers) 105 | -------------------------------------------------------------------------------- /isserviceup/notifiers/notifier.py: -------------------------------------------------------------------------------- 1 | class Notifier(object): 2 | 3 | def notify(self, service, old_status, new_status): 4 | raise NotImplemented() 5 | -------------------------------------------------------------------------------- /isserviceup/notifiers/slack.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from isserviceup.notifiers.notifier import Notifier 3 | 4 | 5 | class Slack(Notifier): 6 | 7 | def __init__(self, web_hook_url): 8 | self.web_hook_url = web_hook_url 9 | 10 | def _build_payload(self, service, old_status, new_status): 11 | from isserviceup.config import config 12 | host_url = config.FRONTEND_URL 13 | if host_url[-1] == '/': 14 | host_url = host_url[:-1] 15 | payload = { 16 | "username": "IsServiceUp.com", 17 | "icon_url": '{}{}'.format(host_url, service.icon_url), 18 | "text": "{}'s new status is <{}|{}> (was {})".format( 19 | service.name, 20 | service.status_url, 21 | new_status, 22 | old_status, 23 | ) 24 | } 25 | return payload 26 | 27 | def notify(self, service, old_status, new_status): 28 | payload = self._build_payload(service, old_status, new_status) 29 | requests.post(self.web_hook_url, json=payload) 30 | 31 | 32 | if __name__ == '__main__': 33 | pass 34 | -------------------------------------------------------------------------------- /isserviceup/services/__init__.py: -------------------------------------------------------------------------------- 1 | from isserviceup.config import config 2 | import importlib 3 | import inspect 4 | import os 5 | import re 6 | from isserviceup.services.models.service import Service 7 | import sys 8 | current_module = sys.modules[__name__] 9 | 10 | 11 | def load_services(): 12 | pysearchre = re.compile('.py$', re.IGNORECASE) 13 | pluginfiles = [x for x in os.listdir(os.path.dirname(__file__)) if pysearchre.search(x)] 14 | form_module = lambda fp: '.' + os.path.splitext(fp)[0] 15 | plugins = map(form_module, pluginfiles) 16 | modules = [] 17 | for plugin in plugins: 18 | if not plugin.startswith('.__'): 19 | modules.append(importlib.import_module(plugin, package=current_module.__name__)) 20 | 21 | services = {} 22 | for module in modules: 23 | for item_name in dir(module): 24 | item = getattr(module, item_name) 25 | if inspect.isclass(item) and issubclass(item, Service) and isinstance(item.name, str): 26 | service_id = item.__name__ 27 | if config.SERVICES is not None and service_id not in config.SERVICES: 28 | continue 29 | if item.name in services: 30 | raise Exception('found multiple service with the name "{}"'.format(item.name)) 31 | services[service_id] = item() 32 | return services 33 | 34 | SERVICES = load_services() 35 | 36 | 37 | if __name__ == '__main__': 38 | print(SERVICES) 39 | -------------------------------------------------------------------------------- /isserviceup/services/atlassian.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Atlassian(StatusPagePlugin): 5 | name = 'Atlassian' 6 | status_url = 'http://status.atlassian.com/' 7 | icon_url = '/images/icons/atlassian.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/authorizedotnet.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class AuthorizeDotNet(StatusPagePlugin): 5 | name = 'Authorize.net' 6 | status_url = 'https://status.authorize.net/' 7 | icon_url = '/images/icons/authorizedotnet.jpg' 8 | -------------------------------------------------------------------------------- /isserviceup/services/aws.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | 4 | from isserviceup.services.models.service import Service, Status 5 | 6 | 7 | class AWS(Service): 8 | name = 'Amazon Web Services' 9 | status_url = 'http://status.aws.amazon.com/' 10 | icon_url = '/images/icons/aws.png' 11 | 12 | def get_status(self): 13 | r = requests.get(self.status_url) 14 | b = BeautifulSoup(r.content, 'html.parser') 15 | tc = str(b.find('table')) 16 | 17 | if ('status0.gif' in tc) or ('status1.gif' in tc): 18 | return Status.ok 19 | elif 'status2.gif' in tc: 20 | return Status.minor 21 | elif 'status3.gif' in tc: 22 | return Status.critical 23 | else: 24 | raise Exception('unexpected status') 25 | -------------------------------------------------------------------------------- /isserviceup/services/azure.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | 4 | from isserviceup.services.models.service import Service, Status 5 | 6 | 7 | class Azure(Service): 8 | name = 'Microsoft Azure' 9 | status_url = 'https://azure.microsoft.com/en-us/status/' 10 | icon_url = '/images/icons/azure.png' 11 | 12 | def get_status(self): 13 | r = requests.get(self.status_url) 14 | b = BeautifulSoup(r.content, 'html.parser') 15 | div = str(b.select_one('.section.section-size3.section-slate09')) 16 | 17 | if 'health-circle' in div or 'health-check' in div or 'health-information' in div: 18 | return Status.ok 19 | elif 'health-warning' in div: 20 | return Status.minor 21 | elif 'health-error' in div: 22 | return Status.critical 23 | else: 24 | raise Exception('unexpected status') 25 | -------------------------------------------------------------------------------- /isserviceup/services/bitbucket.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class BitBucket(StatusPagePlugin): 5 | name = 'BitBucket' 6 | status_url = 'https://status.bitbucket.org/' 7 | icon_url = '/images/icons/bitbucket.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/box.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Box(StatusPagePlugin): 5 | name = 'Box' 6 | status_url = 'https://status.box.com/' 7 | icon_url = '/images/icons/box.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/chargify.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Chargify(StatusPagePlugin): 5 | name = 'Chargify' 6 | status_url = 'http://status.chargify.io/' 7 | icon_url = '/images/icons/chargify.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/circleci.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class CircleCI(StatusPagePlugin): 5 | name = 'CircleCI' 6 | status_url = 'https://status.circleci.com/' 7 | icon_url = '/images/icons/circleci.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/cloudflare.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Cloudflare(StatusPagePlugin): 5 | name = 'Cloudflare' 6 | status_url = 'http://www.cloudflarestatus.com/' 7 | icon_url = '/images/icons/cloudflare.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/codeclimate.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class CodeClimate(StatusPagePlugin): 5 | name = 'Code Climate' 6 | status_url = 'http://status.codeclimate.com/' 7 | icon_url = '/images/icons/codeclimate.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/codeship.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class CodeShip(StatusPagePlugin): 5 | name = 'Codeship' 6 | status_url = 'http://codeshipstatus.com/' 7 | icon_url = '/images/icons/codeship.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/compose.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Compose(StatusPagePlugin): 5 | name = 'Compose' 6 | status_url = 'https://status.compose.com/' 7 | icon_url = '/images/icons/compose.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/datadog.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class DataDog(StatusPagePlugin): 5 | name = 'DataDog' 6 | status_url = 'https://status.datadoghq.com/' 7 | icon_url = '/images/icons/datadog.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/digitalocean.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class DigitalOcean(StatusPagePlugin): 5 | name = 'DigitalOcean' 6 | status_url = 'https://status.digitalocean.com' 7 | icon_url = '/images/icons/do.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/disqus.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Disqus(StatusPagePlugin): 5 | name = 'Disqus' 6 | status_url = 'https://status.disqus.com/' 7 | icon_url = '/images/icons/disqus.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/dnsimple.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Dnsimple(StatusPagePlugin): 5 | name = 'dnsimple' 6 | status_url = 'http://dnsimplestatus.com/' 7 | icon_url = '/images/icons/dnsimple.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/docker.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statusio import StatusIOPlugin 2 | 3 | 4 | class Docker(StatusIOPlugin): 5 | name = 'Docker' 6 | status_url = 'https://status.docker.com/' 7 | statuspage_id = '533c6539221ae15e3f000031' 8 | icon_url = '/images/icons/docker.png' 9 | -------------------------------------------------------------------------------- /isserviceup/services/dropbox.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Dropbox(StatusPagePlugin): 5 | name = 'Dropbox' 6 | status_url = 'https://status.dropbox.com' 7 | icon_url = '/images/icons/dropbox.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/duo.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Duo(StatusPagePlugin): 5 | name = 'Duo' 6 | status_url = 'https://status.duo.com/' 7 | icon_url = '/images/icons/duo.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/dyn.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Dyn(StatusPagePlugin): 5 | name = 'Dyn' 6 | status_url = 'https://www.dynstatus.com/' 7 | icon_url = '/images/icons/dyn.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/fastly.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Fastly(StatusPagePlugin): 5 | name = 'Fastly' 6 | status_url = 'https://status.fastly.com/' 7 | icon_url = '/images/icons/fastly.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/ftrack.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class FTrack(StatusPagePlugin): 5 | name = 'FTrack' 6 | status_url = 'http://status.ftrack.com/' 7 | icon_url = '/images/icons/ftrack.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/gcloud.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | 4 | from isserviceup.services.models.service import Service, Status 5 | 6 | 7 | class GCloud(Service): 8 | name = 'Google Cloud' 9 | status_url = 'https://status.cloud.google.com/' 10 | icon_url = '/images/icons/gcloud.png' 11 | 12 | def get_status(self): 13 | r = requests.get(self.status_url) 14 | b = BeautifulSoup(r.content, 'html.parser') 15 | status = next(x for x in b.find(class_='subheader').attrs['class'] 16 | if x.startswith('open-incident-bar-')) 17 | 18 | if status == 'open-incident-bar-clear': 19 | return Status.ok 20 | elif status == 'open-incident-bar-medium': 21 | return Status.major 22 | elif status == 'open-incident-bar-high': 23 | return Status.critical 24 | else: 25 | raise Exception('unexpected status') 26 | -------------------------------------------------------------------------------- /isserviceup/services/github.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from isserviceup.services.models.service import Service, Status 4 | 5 | GitHubStatusMap = { 6 | 'good': Status.ok, 7 | 'major': Status.major, 8 | 'minor': Status.minor 9 | } 10 | 11 | 12 | class GitHub(Service): 13 | name = 'GitHub' 14 | status_url = 'https://status.github.com/' 15 | icon_url = '/images/icons/github.png' 16 | 17 | def get_status(self): 18 | r = requests.get(self.status_url + 'api/status.json') 19 | res = r.json() 20 | status = res['status'] 21 | return GitHubStatusMap[status] 22 | -------------------------------------------------------------------------------- /isserviceup/services/gitlab.py: -------------------------------------------------------------------------------- 1 | from decouple import config 2 | from isserviceup.services.models.gitlab import GitLabPlugin 3 | 4 | 5 | class GitLab(GitLabPlugin): 6 | 7 | name = config('GITLAB_SERVICE_NAME', default='GitLab') 8 | status_url = config('GITLAB_URL', default='https://gitlab.com/') 9 | -------------------------------------------------------------------------------- /isserviceup/services/gocardless.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class GoCardless(StatusPagePlugin): 5 | name = 'Go Cardless' 6 | status_url = 'https://www.gocardless-status.com/' 7 | icon_url = '/images/icons/gocardless.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/gotomeeting.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class GotoMeeting(StatusPagePlugin): 5 | name = 'GoToMeeting' 6 | status_url = 'http://status.gotomeeting.com/' 7 | icon_url = '/images/icons/gotomeeting.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/harvest.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Harvest(StatusPagePlugin): 5 | name = 'Harvest' 6 | status_url = 'http://www.harveststatus.com' 7 | icon_url = '/images/icons/harvest.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/hashicorp.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class HashiCorp(StatusPagePlugin): 5 | name = 'HashiCorp' 6 | status_url = 'https://status.hashicorp.com/' 7 | icon_url = '/images/icons/hashicorp.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/heroku.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from isserviceup.services.models.service import Service, Status 4 | 5 | 6 | class Heroku(Service): 7 | name = 'Heroku' 8 | status_url = 'https://status.heroku.com/' 9 | icon_url = '/images/icons/heroku.png' 10 | 11 | def get_status(self): 12 | r = requests.get('https://status.heroku.com/api/v3/current-status') 13 | res = r.json() 14 | status = res['status']['Production'] 15 | 16 | if status == 'green': 17 | return Status.ok 18 | elif status == 'yellow': 19 | return Status.minor 20 | elif status == 'orange': 21 | return Status.major 22 | elif status == 'red': 23 | return Status.critical 24 | else: 25 | raise Exception('unexpected status') 26 | 27 | -------------------------------------------------------------------------------- /isserviceup/services/honeybadger.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Honeybadger(StatusPagePlugin): 5 | name = 'Honeybadger' 6 | status_url = 'http://status.honeybadger.io/' 7 | icon_url = '/images/icons/honeybadger.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/linode.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Linode(StatusPagePlugin): 5 | name = 'Linode' 6 | status_url = 'http://status.linode.com/' 7 | icon_url = '/images/icons/linode.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/loggly.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Loggly(StatusPagePlugin): 5 | name = 'Loggly' 6 | status_url = 'http://status.loggly.com//' 7 | icon_url = '/images/icons/loggly.jpg' 8 | -------------------------------------------------------------------------------- /isserviceup/services/mailgun.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class MailGun(StatusPagePlugin): 5 | name = 'MailGun' 6 | status_url = 'http://status.mailgun.com/' 7 | icon_url = '/images/icons/mailgun.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/maxcdn.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class MaxCDN(StatusPagePlugin): 5 | name = 'MaxCDN' 6 | status_url = 'https://status.maxcdn.com/' 7 | icon_url = '/images/icons/maxcdn.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/isserviceup/services/models/__init__.py -------------------------------------------------------------------------------- /isserviceup/services/models/gitlab.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import subprocess 3 | from decouple import config 4 | from isserviceup.config import config as isu_config 5 | from isserviceup.services.models.service import Service, Status 6 | try: 7 | from urlparse import urlparse 8 | except ImportError: 9 | from urllib.parse import urlparse 10 | 11 | 12 | class GitLabPlugin(Service): 13 | 14 | icon_url = '/images/icons/gitlab.png' 15 | 16 | def __init__(self): 17 | # Will check if the GitLab website is OK. 18 | self.check_http = config('GITLAB_CHECK_HTTP', default=True, cast=bool) 19 | # Will check if the GitLab JSON API is OK. 20 | self.check_json = config('GITLAB_CHECK_JSON', default=True, cast=bool) 21 | # Will check if the SSH connections to git@{gitlab_url} are OK. 22 | # To use this you need to set the PRIVATE_SSH_KEY option in the config. 23 | self.check_ssh = config('GITLAB_CHECK_SSH', default=False, cast=bool) 24 | # If you need to monitor multiple GitLab instances and need each one to 25 | # have a different behaviour: extend this class, override the __init__ 26 | # function, call super, and then override these properties. 27 | 28 | def _check_http(self): 29 | request = requests.get(self.status_url) 30 | return request.ok 31 | 32 | def _check_json(self): 33 | request = requests.get('{base}/api/v4/projects'.format( 34 | base=self.status_url.strip('/') 35 | )) 36 | try: 37 | if request.json(): 38 | return True 39 | except ValueError as error: 40 | print('Value error: {msg}'.format(msg=error.message)) 41 | return False 42 | 43 | def _check_private_ssh_key(self): 44 | if not isu_config.ensure_private_ssh_key(): 45 | print('GitLab SSH error for {url}: {error}'.format( 46 | url=self.status_url, 47 | error='No private SSH key found. Make sure the file path you ' 48 | 'set in option PRIVATE_SSH_KEY is correct. If it is a ' 49 | 'shared file, make sure it has permissions to be read ' 50 | 'by other users.')) 51 | return False 52 | return True 53 | 54 | def _check_ssh(self): 55 | if not self._check_private_ssh_key(): 56 | return False 57 | url = urlparse(self.status_url) 58 | try: 59 | ssh = subprocess.check_output([ 60 | 'ssh', '-T', '-o StrictHostKeyChecking=no', 61 | 'git@{host}'.format(host=url.netloc) 62 | ]) 63 | except subprocess.CalledProcessError as error: 64 | print('GitLab SSH error for {url}: {error}'.format( 65 | url=self.status_url, error=error)) 66 | return False 67 | return 'Welcome to GitLab' in str(ssh) 68 | 69 | def get_status(self): 70 | status = [ 71 | not self.check_http or self._check_http(), 72 | not self.check_json or self._check_json(), 73 | not self.check_ssh or self._check_ssh(), 74 | ] 75 | if all(status): 76 | return Status.ok 77 | elif any(status): 78 | return Status.major 79 | return Status.critical 80 | -------------------------------------------------------------------------------- /isserviceup/services/models/service.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Status(Enum): 5 | ok = 1 # green 6 | maintenance = 2 # blue 7 | minor = 3 # yellow 8 | major = 4 # orange 9 | critical = 5 # red 10 | unavailable = 6 # gray 11 | 12 | 13 | class Service(object): 14 | 15 | @property 16 | def id(self): 17 | return self.__class__.__name__ 18 | 19 | @property 20 | def status_url(self): 21 | raise NotImplemented() 22 | 23 | @property 24 | def icon_url(self): 25 | raise NotImplemented() 26 | 27 | @property 28 | def name(self): 29 | raise NotImplemented() 30 | 31 | def get_status(self): 32 | raise NotImplemented() 33 | -------------------------------------------------------------------------------- /isserviceup/services/models/statusio.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from isserviceup.services.models.service import Service, Status 4 | 5 | 6 | class StatusIOPlugin(Service): 7 | 8 | status_url = 'https://api.status.io/' 9 | 10 | @property 11 | def statuspage_id(self): 12 | raise NotImplemented() 13 | 14 | def get_status(self): 15 | r = requests.get('{}/1.0/status/{}'.format( 16 | self.status_url.strip("/"), self.statuspage_id 17 | )) 18 | try: 19 | j = r.json() 20 | except ValueError: 21 | print(r.content) 22 | raise 23 | expected_status = { 24 | 100: Status.ok, 25 | 200: Status.minor, 26 | 300: Status.major, 27 | 400: Status.critical, 28 | 500: Status.critical, 29 | 600: Status.maintenance, 30 | } 31 | try: 32 | status_code = j['result']['status_overall']['status_code'] 33 | if status_code in expected_status: 34 | return expected_status[status_code] 35 | raise Exception('unexpected status') 36 | except KeyError: 37 | print(j) 38 | raise 39 | -------------------------------------------------------------------------------- /isserviceup/services/models/statuspage.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | 4 | from isserviceup.services.models.service import Service, Status 5 | 6 | 7 | class StatusPagePlugin(Service): 8 | 9 | def get_status(self): 10 | r = requests.get(self.status_url) 11 | b = BeautifulSoup(r.content, 'html.parser') 12 | page_status = b.find(class_=['status', 'index']) 13 | try: 14 | status = next(x for x in page_status.attrs['class'] if x.startswith('status-')) 15 | 16 | if status == 'status-none': 17 | return Status.ok 18 | elif status == 'status-critical': 19 | return Status.critical 20 | elif status == 'status-major': 21 | return Status.major 22 | elif status == 'status-minor': 23 | return Status.minor 24 | elif status == 'status-maintenance': 25 | return Status.maintenance 26 | else: 27 | raise Exception('unexpected status') 28 | except AttributeError as e: 29 | print(b) 30 | raise AttributeError(e) -------------------------------------------------------------------------------- /isserviceup/services/models/weblate.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from decouple import config 3 | from isserviceup.services.models.service import Service, Status 4 | 5 | 6 | class WeblatePlugin(Service): 7 | 8 | icon_url = '/images/icons/weblate.png' 9 | 10 | def __init__(self): 11 | # Will check if the Weblate website is OK. 12 | self.check_http = config('WEBLATE_CHECK_HTTP', default=True, cast=bool) 13 | # Will check if the Weblate JSON API is OK. 14 | self.check_json = config('WEBLATE_CHECK_JSON', default=True, cast=bool) 15 | # If you need to monitor multiple Weblate instances and need each one 16 | # to have a different behaviour: extend this class, override the 17 | # __init__ function, call super, and then override these properties. 18 | 19 | def _check_http(self): 20 | request = requests.get(self.status_url) 21 | return request.ok 22 | 23 | def _check_json(self): 24 | request = requests.get('{base}/api/projects'.format( 25 | base=self.status_url.strip('/') 26 | )) 27 | try: 28 | if request.json(): 29 | return True 30 | except ValueError as error: 31 | print('Value error: {msg}'.format(msg=error.message)) 32 | return False 33 | 34 | def get_status(self): 35 | status = [ 36 | not self.check_http or self._check_http(), 37 | not self.check_json or self._check_json(), 38 | ] 39 | if all(status): 40 | return Status.ok 41 | elif any(status): 42 | return Status.major 43 | return Status.critical 44 | -------------------------------------------------------------------------------- /isserviceup/services/newrelic.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class NewRelic(StatusPagePlugin): 5 | name = 'New Relic' 6 | status_url = 'https://status.newrelic.com/' 7 | icon_url = '/images/icons/newrelic.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/npm.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class NPM(StatusPagePlugin): 5 | name = 'NPM' 6 | status_url = 'http://status.npmjs.org/' 7 | icon_url = '/images/icons/npm.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/opbeat.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class OpBeat(StatusPagePlugin): 5 | name = 'opbeat' 6 | status_url = 'http://status.opbeat.com/' 7 | icon_url = '/images/icons/opbeat.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/packagecloud.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class PackageCloud(StatusPagePlugin): 5 | name = 'Package Cloud' 6 | status_url = 'http://www.packagecloudstatus.io/' 7 | icon_url = '/images/icons/packagecloud.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/pagerduty.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class PagerDuty(StatusPagePlugin): 5 | name = 'PagerDuty' 6 | status_url = 'https://status.pagerduty.com/' 7 | icon_url = '/images/icons/pagerduty.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/pingdom.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Pingdom(StatusPagePlugin): 5 | name = 'Pingdom' 6 | status_url = 'http://status.pingdom.com/' 7 | icon_url = '/images/icons/pingdom.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/pingidentity.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class PingIdentity(StatusPagePlugin): 5 | name = 'Ping Identity' 6 | status_url = 'https://status.pingidentity.com/' 7 | icon_url = '/images/icons/pingidentity.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/pusher.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Pusher(StatusPagePlugin): 5 | name = 'Pusher' 6 | status_url = 'https://status.pusher.com/' 7 | icon_url = '/images/icons/pusher.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/pypi.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class PythonInfra(StatusPagePlugin): 5 | name = 'Python Infrastructure' 6 | status_url = 'https://status.python.org/' 7 | icon_url = '/images/icons/pyinfra.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/quay.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Quay(StatusPagePlugin): 5 | name = 'Quay' 6 | status_url = 'http://status.quay.io/' 7 | icon_url = '/images/icons/quay.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/redislabs.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class RedisLabs(StatusPagePlugin): 5 | name = 'RedisLabs' 6 | status_url = 'https://status.redislabs.com/' 7 | icon_url = '/images/icons/redislabs.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/rollbar.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class RollBar(StatusPagePlugin): 5 | name = 'RollBar' 6 | status_url = 'http://status.rollbar.com/' 7 | icon_url = '/images/icons/rollbar.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/rubygems.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class RubyGems(StatusPagePlugin): 5 | name = 'Ruby Gems' 6 | status_url = 'https://status.rubygems.org/' 7 | icon_url = '/images/icons/rubygems.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/sendgrid.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class SendGrid(StatusPagePlugin): 5 | name = 'SendGrid' 6 | status_url = 'http://status.sendgrid.com/' 7 | icon_url = '/images/icons/sendgrid.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/sendwithus.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class SendWithUs(StatusPagePlugin): 5 | name = 'Send with us' 6 | status_url = 'https://status.sendwithus.com/' 7 | icon_url = '/images/icons/sendwithus.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/sentry.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Sentry(StatusPagePlugin): 5 | name = 'Sentry' 6 | status_url = 'https://status.sentry.io/' 7 | icon_url = '/images/icons/sentry.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/shotgun.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Shotgun(StatusPagePlugin): 5 | name = 'Shotgun' 6 | status_url = 'http://status.shotgunsoftware.com/' 7 | icon_url = '/images/icons/shotgun.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/slack.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | 4 | from isserviceup.services.models.service import Service, Status 5 | 6 | 7 | class Slack(Service): 8 | name = 'Slack' 9 | status_url = 'https://status.slack.com/' 10 | icon_url = '/images/icons/slack.png' 11 | 12 | def get_status(self): 13 | r = requests.get(self.status_url) 14 | b = BeautifulSoup(r.content, 'html.parser') 15 | div = b.select_one('.current_status') 16 | 17 | status_classes = div.attrs['class'] 18 | 19 | if 'all_clear' in status_classes: 20 | return Status.ok 21 | elif 'issue' in status_classes: 22 | return Status.major 23 | elif 'maintenance' in status_classes: 24 | return Status.maintenance 25 | else: 26 | raise Exception('unexpected status') 27 | -------------------------------------------------------------------------------- /isserviceup/services/sparkpost.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class SparkPost(StatusPagePlugin): 5 | name = 'SparkPost' 6 | status_url = 'http://status.sparkpost.com/' 7 | icon_url = '/images/icons/sparkpost.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/statusio.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statusio import StatusIOPlugin 2 | 3 | 4 | class StatusIO(StatusIOPlugin): 5 | name = 'Status.io' 6 | status_url = 'https://status.status.io/' 7 | statuspage_id = '51f6f2088643809b7200000d' 8 | icon_url = '/images/icons/statusio.png' 9 | -------------------------------------------------------------------------------- /isserviceup/services/statuspage.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class StatusPage(StatusPagePlugin): 5 | name = 'StatusPage' 6 | status_url = 'http://metastatuspage.com/' 7 | icon_url = '/images/icons/statuspage.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/stormpath.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class StormPath(StatusPagePlugin): 5 | name = 'StormPath' 6 | status_url = 'https://status.stormpath.com/' 7 | icon_url = '/images/icons/stormpath.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/stripe.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | 4 | from isserviceup.services.models.service import Service, Status 5 | 6 | 7 | class Stripe(Service): 8 | name = 'Stripe' 9 | status_url = 'https://status.stripe.com/' 10 | icon_url = '/images/icons/stripe.png' 11 | 12 | def get_status(self): 13 | r = requests.get(self.status_url) 14 | b = BeautifulSoup(r.content, 'html.parser') 15 | div = b.select_one('.status-bubble') 16 | 17 | status_classes = div.attrs['class'] 18 | 19 | if 'status-up' in status_classes: 20 | return Status.ok 21 | elif 'status-down' in status_classes: 22 | return Status.critical 23 | else: 24 | raise Exception('unexpected status') 25 | -------------------------------------------------------------------------------- /isserviceup/services/travis.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Travis(StatusPagePlugin): 5 | name = 'Travis' 6 | status_url = 'https://www.traviscistatus.com/' 7 | icon_url = '/images/icons/travis.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/twilio.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class Twilio(StatusPagePlugin): 5 | name = 'Twilio' 6 | status_url = 'https://status.twilio.com/' 7 | icon_url = '/images/icons/twilio.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/victorops.py: -------------------------------------------------------------------------------- 1 | from isserviceup.services.models.statuspage import StatusPagePlugin 2 | 3 | 4 | class VictorOps(StatusPagePlugin): 5 | name = 'VictorOps' 6 | status_url = 'http://victorops.statuspage.io/' 7 | icon_url = '/images/icons/victorops.png' 8 | -------------------------------------------------------------------------------- /isserviceup/services/vultr.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | 4 | from isserviceup.services.models.service import Service, Status 5 | 6 | 7 | class Vultr(Service): 8 | name = 'Vultr' 9 | status_url = 'https://www.vultr.com/status' 10 | icon_url = '/images/icons/vultr.png' 11 | 12 | def get_status(self): 13 | r = requests.get(self.status_url) 14 | b = BeautifulSoup(r.content, 'html.parser') 15 | div = b.find('div', {'class': 'row'}) 16 | status = div.findAll('i', {'class': 'zmdi'}) 17 | if all('text-sucess' in str(s) for s in status): 18 | return Status.ok 19 | elif (any('text-warning' in str(s) for s in status) 20 | or any('text-danger' in str(s) for s in status)): 21 | return Status.minor 22 | elif all('text-danger' in str(s) for s in status): 23 | return Status.critical 24 | else: 25 | raise Exception('unexpected status') 26 | -------------------------------------------------------------------------------- /isserviceup/services/weblate.py: -------------------------------------------------------------------------------- 1 | from decouple import config 2 | from isserviceup.services.models.weblate import WeblatePlugin 3 | 4 | 5 | class Weblate(WeblatePlugin): 6 | 7 | name = config('WEBLATE_SERVICE_NAME', default='Weblate') 8 | status_url = config('WEBLATE_URL', default='https://demo.weblate.com/') 9 | -------------------------------------------------------------------------------- /isserviceup/storage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/isserviceup/storage/__init__.py -------------------------------------------------------------------------------- /isserviceup/storage/favorites.py: -------------------------------------------------------------------------------- 1 | from isserviceup.models.favorite import Favorite 2 | from isserviceup.services import SERVICES 3 | 4 | 5 | def get_favorite_services(user_id): 6 | favs = Favorite.objects(user_id=user_id) 7 | res = [] 8 | for fav in favs: 9 | res.append(SERVICES[fav.service_id]) 10 | return res 11 | 12 | 13 | def update_favorite_status(user_id, service_id, status): 14 | if status: # added favorite 15 | Favorite(user_id=user_id, service_id=service_id).save() 16 | else: # removed favorite 17 | Favorite.objects(user_id=user_id, service_id=service_id).delete() 18 | -------------------------------------------------------------------------------- /isserviceup/storage/services.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from isserviceup.services.models.service import Status 4 | 5 | 6 | _status_key = lambda service: 'service:{}'.format(service.name) 7 | _last_update_key = 'services:last_update' 8 | 9 | 10 | def set_last_update(client, t): 11 | client.set(_last_update_key, t) 12 | 13 | 14 | def set_service_status(client, service, status): 15 | key = _status_key(service) 16 | pipe = client.pipeline() 17 | pipe.hget(key, 'status') 18 | pipe.hmset(key, { 19 | 'status': status.name, 20 | 'last_update': time.time(), 21 | }) 22 | prev_status = pipe.execute()[0] 23 | if prev_status is not None: 24 | prev_status = Status[prev_status] 25 | return prev_status 26 | 27 | 28 | def get_status(client, services): 29 | pipe = client.pipeline() 30 | for service in services: 31 | pipe.hget(_status_key(service), 'status') 32 | values = pipe.execute() 33 | status = [Status[x] if x else None for x in values] 34 | return status 35 | -------------------------------------------------------------------------------- /isserviceup/storage/sessions.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from isserviceup import managers 4 | from isserviceup.helpers import utils 5 | 6 | 7 | SESSION_TTL = 60*60*24*30 8 | _session_key = lambda x: 'session:{}'.format(x) 9 | 10 | 11 | def get(sid): 12 | res = managers.rclient.get(_session_key(sid)) 13 | if not res: 14 | return None 15 | res = json.loads(res) 16 | return res 17 | 18 | 19 | def create(data): 20 | sid = utils.random_string(16) 21 | data['sid'] = sid 22 | managers.rclient.set(_session_key(sid), json.dumps(data), ex=SESSION_TTL) 23 | return sid 24 | 25 | 26 | def destroy(sid): 27 | managers.rclient.delete(_session_key(sid)) 28 | return True 29 | -------------------------------------------------------------------------------- /isserviceup/storage/users.py: -------------------------------------------------------------------------------- 1 | from isserviceup.models.user import User 2 | 3 | 4 | def upsert_user(avatar_url, username, github_access_token): 5 | return User.objects(username=username).upsert_one(set__avatar_url=avatar_url, 6 | set__github_access_token=github_access_token) 7 | 8 | 9 | def get_user(user_id): 10 | return User.objects.with_id(user_id) 11 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | requests 2 | bs4 3 | flask 4 | flask-cors 5 | python-decouple 6 | redis 7 | gunicorn 8 | eventlet 9 | celery 10 | raven 11 | blinker 12 | gevent 13 | mongoengine 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file requirements.txt requirements.in 6 | # 7 | amqp==1.4.9 # via kombu 8 | anyjson==0.3.3 # via kombu 9 | beautifulsoup4==4.5.1 # via bs4 10 | billiard==3.3.0.23 # via celery 11 | blinker==1.4 12 | bs4==0.0.1 13 | celery==3.1.24 14 | click==6.6 # via flask 15 | contextlib2==0.5.4 # via raven 16 | eventlet==0.19.0 17 | flask-cors==3.0.2 18 | flask==0.11.1 19 | gevent==1.1.2 20 | greenlet==0.4.10 # via eventlet, gevent 21 | gunicorn==19.6.0 22 | itsdangerous==0.24 # via flask 23 | Jinja2==2.8 # via flask 24 | kombu==3.0.37 # via celery 25 | MarkupSafe==0.23 # via jinja2 26 | mongoengine==0.10.6 27 | pymongo==3.3.1 # via mongoengine 28 | python-decouple==3.0 29 | pytz==2016.7 # via celery 30 | raven==5.31.0 31 | redis==2.10.5 32 | requests==2.11.1 33 | Six==1.10.0 # via flask-cors 34 | Werkzeug==0.11.11 # via flask 35 | -------------------------------------------------------------------------------- /shared/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcopaz/is-service-up/9df5ebb1fbb09102300518855aaa35c0a84bb2bf/shared/.gitkeep --------------------------------------------------------------------------------