├── .github ├── CODEOWNERS └── workflows │ └── tests.yml ├── .gitignore ├── AUTHORS ├── CONTRIBUTING.rst ├── CONTRIBUTORS ├── Dockerfile ├── LICENSE ├── Makefile ├── README.rst ├── consumer ├── definitions.json ├── kafka_consumer │ ├── __init__.py │ └── utils.py ├── main.py └── schemas.json ├── depc.example.yml ├── depc ├── __init__.py ├── admin.py ├── apiv1 │ ├── __init__.py │ ├── checks.py │ ├── configs.py │ ├── dependencies.py │ ├── errors.py │ ├── news.py │ ├── qos.py │ ├── rules.py │ ├── sources.py │ ├── statistics.py │ ├── teams.py │ ├── users.py │ └── variables.py ├── commands │ ├── __init__.py │ ├── config.py │ ├── key.py │ └── user.py ├── context.py ├── controllers │ ├── __init__.py │ ├── checks.py │ ├── configs.py │ ├── dependencies.py │ ├── grants.py │ ├── news.py │ ├── qos.py │ ├── rules.py │ ├── sources.py │ ├── statistics.py │ ├── teams.py │ ├── users.py │ ├── variables.py │ └── worst.py ├── extensions │ ├── __init__.py │ ├── encrypted_dict.py │ └── flask_redis_cache.py ├── logs │ ├── __init__.py │ ├── handlers.py │ └── sinks.py ├── models │ ├── __init__.py │ ├── checks.py │ ├── configs.py │ ├── news.py │ ├── rules.py │ ├── sources.py │ ├── teams.py │ ├── users.py │ ├── variables.py │ └── worst.py ├── queries.py ├── schemas │ ├── v1_check.json │ ├── v1_config.json │ ├── v1_label.json │ ├── v1_rule.json │ ├── v1_source.json │ ├── v1_team.json │ ├── v1_variable.json │ └── v1_warp.json ├── sources │ ├── __init__.py │ ├── exceptions.py │ ├── fake │ │ ├── __init__.py │ │ └── metrics.py │ ├── opentsdb │ │ └── __init__.py │ └── warp │ │ └── __init__.py ├── static │ ├── grafana_details_dashboard.json │ └── grafana_summary_dashboard.json ├── tasks.py ├── templates.py ├── templates │ └── admin │ │ └── cache.html ├── users.py └── utils │ ├── __init__.py │ ├── neo4j.py │ ├── qos.py │ ├── redis_cache.py │ └── warp10.py ├── depc_dependencies.png ├── depc_logo.png ├── depc_qos.png ├── depc_screenshots.gif ├── depc_screenshots_small.gif ├── docker-entrypoint.sh ├── docs ├── Makefile ├── _static │ ├── css │ │ └── custom.css │ ├── custom.css │ └── images │ │ ├── architecture.png │ │ ├── architecture.svg │ │ ├── depc-logo.png │ │ ├── depc-logo.svg │ │ ├── depc_screenshots.gif │ │ ├── guides │ │ ├── grafana │ │ │ ├── details.png │ │ │ ├── export_button.png │ │ │ ├── export_modal.png │ │ │ ├── import_json.png │ │ │ ├── import_json_2.png │ │ │ └── summary.png │ │ ├── indicators │ │ │ ├── interval.png │ │ │ └── simple_threshold.png │ │ └── variables │ │ │ ├── builtins_variables.png │ │ │ ├── indicator_variable.png │ │ │ ├── jinja_condition_1.png │ │ │ ├── jinja_condition_2.png │ │ │ └── jinja_condition_3.png │ │ ├── installation │ │ ├── airflow_webserver.png │ │ ├── airflow_webserver_config.png │ │ ├── create_team.png │ │ └── empty_homepage.png │ │ ├── logo.png │ │ ├── neo4j-logo.png │ │ ├── qos_dependencies.png │ │ └── tutorial │ │ ├── acme_team.png │ │ ├── all_fake_checks.png │ │ ├── associate_filer_servers.png │ │ ├── attach_server_ping_check.png │ │ ├── attach_servers_checks.png │ │ ├── check_server_ping.png │ │ ├── dashboard1.png │ │ ├── dashboard2.png │ │ ├── dashboard3.png │ │ ├── dependencies.png │ │ ├── dependencies_association.png │ │ ├── dependencies_qos.png │ │ ├── dependencies_website_filer.png │ │ ├── empty_dashboard.png │ │ ├── graph_api.png │ │ ├── kafka1.png │ │ ├── kafka2.png │ │ ├── kafka3.png │ │ ├── new_fake_source.png │ │ ├── new_filers_rule.png │ │ ├── qos_evolution_all_labels.png │ │ ├── qos_evolution_specific_label.png │ │ ├── qos_evolution_specific_node.png │ │ ├── qos_evolution_specific_node_details1.png │ │ ├── qos_evolution_specific_node_details2.png │ │ ├── rule_details_http_status_code.png │ │ ├── rule_details_max_dbconnections.png │ │ ├── rule_details_server_oco.png │ │ ├── rule_details_server_ping.png │ │ ├── rule_filers_result.png │ │ ├── rule_launched_summary.png │ │ ├── rule_range_yesterday.png │ │ ├── team_id.png │ │ ├── update_configuration.png │ │ └── website_dependencies.png ├── api │ ├── checks.rst │ ├── configs.rst │ ├── dependencies.rst │ ├── index.rst │ ├── news.rst │ ├── qos.rst │ ├── rules.rst │ ├── sources.rst │ ├── statistics.rst │ ├── teams.rst │ ├── users.rst │ └── variables.rst ├── conf.py ├── guides │ ├── difference-qos-sla.rst │ ├── grafana.rst │ ├── index.rst │ ├── indicators.rst │ ├── kafka.rst │ ├── queries.rst │ ├── sources.rst │ └── variables.rst ├── index.rst ├── installation.rst ├── make.bat └── tutorial.rst ├── manage.py ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ ├── 30126831f6eb_init_database.py │ ├── 48570234e11c_add_the_news_table.py │ ├── 88c2ab34728c_rename_check_parameters.py │ ├── 956e9a3dd287_add_metas_column_to_teams_table.py │ └── d2dc7ff020a0_remove_logs_table.py ├── pytest.ini ├── requirements.txt ├── scheduler ├── __init__.py └── dags │ ├── __init__.py │ ├── decoder.py │ ├── generate_dags.py │ ├── operators │ ├── __init__.py │ ├── after_subdag_operator.py │ ├── aggregation_operator.py │ ├── average_operator.py │ ├── before_subdag_operator.py │ ├── daily_worst_operator.py │ ├── operation_operator.py │ └── rule_operator.py │ └── save_config.py ├── tests ├── __init__.py ├── apiv1 │ ├── __init__.py │ ├── test_checks.py │ ├── test_dependencies.py │ ├── test_news.py │ ├── test_ping.py │ ├── test_rules.py │ ├── test_sources.py │ └── test_teams.py ├── conftest.py ├── consumer │ ├── __init__.py │ └── test_utils.py ├── controllers │ ├── __init__.py │ ├── test_configs.py │ ├── test_dependencies.py │ └── test_teams.py ├── data │ ├── available_sources.json │ ├── opentsdb_checks.json │ ├── opentsdb_source.json │ └── opentsdb_threshold_check.json └── utils │ ├── __init__.py │ └── test_qos.py └── ui ├── .bowerrc ├── .editorconfig ├── .npmrc ├── Dockerfile ├── Gruntfile.js ├── README.md ├── app ├── 404.html ├── favicon.ico ├── images │ ├── alert.png │ ├── bg.gif │ ├── empty.png │ ├── fake.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── gray-line-bg.gif │ ├── loading.gif │ ├── logo.png │ ├── opentsdb.png │ ├── panel-critical.png │ ├── panel-ok.png │ ├── panel-unknown.png │ ├── panel-warning.png │ └── warp10.png ├── index.html ├── robots.txt ├── scripts │ ├── app.js │ ├── controllers │ │ ├── display_check_parameters.js │ │ ├── modal_associate_checks.js │ │ ├── modal_display_check_result.js │ │ ├── modal_display_config.js │ │ ├── modal_display_json.js │ │ ├── modal_edit_rule.js │ │ ├── modal_edit_source.js │ │ ├── modal_new_config.js │ │ ├── modal_new_rule.js │ │ ├── modal_new_source.js │ │ ├── modal_news.js │ │ ├── modal_relationship_periods.js │ │ ├── navbar.js │ │ ├── team │ │ │ ├── checks.js │ │ │ ├── configuration.js │ │ │ ├── dashboard.js │ │ │ ├── dependencies.js │ │ │ ├── item.js │ │ │ ├── label.js │ │ │ ├── permissions.js │ │ │ ├── rules.js │ │ │ ├── statistics.js │ │ │ └── variables.js │ │ └── teams.js │ ├── filters │ │ ├── numberTrunc.js │ │ └── slugify.js │ └── services │ │ ├── breadcrumbs.js │ │ ├── charts.js │ │ ├── checks.js │ │ ├── config.js │ │ ├── configurations.js │ │ ├── dependencies.js │ │ ├── httpinterceptor.js │ │ ├── modal.js │ │ ├── news.js │ │ ├── qos.js │ │ ├── rules.js │ │ ├── sources.js │ │ ├── statistics.js │ │ ├── teams.js │ │ ├── users.js │ │ └── variables.js ├── styles │ └── main.css └── views │ ├── modals │ ├── associate_checks.html │ ├── confirm.html │ ├── display_check_parameters.html │ ├── display_check_result.html │ ├── display_config.html │ ├── display_json.html │ ├── edit_rule.html │ ├── edit_source.html │ ├── new_config.html │ ├── new_rule.html │ ├── new_source.html │ ├── news.html │ └── relationship_periods.html │ ├── navbar.html │ ├── team │ ├── checks.html │ ├── configuration.html │ ├── dashboard.html │ ├── dependencies.html │ ├── item.html │ ├── label.html │ ├── navbar.html │ ├── permissions.html │ ├── rules.html │ ├── statistics.html │ └── variables.html │ └── teams.html ├── bower.json ├── nginx └── default └── package.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default reviewers 2 | * @ncrocfer @anthonyolea 3 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-18.04 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: actions/setup-python@v2 10 | with: 11 | python-version: 3.7 12 | architecture: x64 13 | - name: Check code with Python Black 14 | run: | 15 | pip install black==19.10b0 16 | black --check depc consumer scheduler 17 | 18 | tests: 19 | runs-on: ubuntu-18.04 20 | services: 21 | neo4j: 22 | image: neo4j:3.4.18 23 | env: 24 | NEO4J_AUTH: neo4j/foobar 25 | NEO4J_dbms_connector_bolt_advertised__address: localhost:7687 26 | NEO4J_dbms_connector_http_advertised__address: localhost:7474 27 | ports: 28 | - 7687:7687 29 | - 7474:7474 30 | strategy: 31 | matrix: 32 | python-version: [3.6, 3.7] 33 | steps: 34 | - uses: actions/checkout@v2 35 | - uses: actions/setup-python@v2 36 | with: 37 | python-version: ${{ matrix.python-version }} 38 | architecture: x64 39 | - name : Install packages 40 | run: | 41 | sudo apt-get install libsnappy-dev 42 | pip install python-snappy==0.5.4 43 | pip install -r requirements.txt 44 | - name: Execute tests 45 | run: make tests 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Configuration 2 | depc.prod.yml 3 | depc.production.yml 4 | depc.dev.yml 5 | depc.development.yml 6 | depc.test.yml 7 | depc.testing.yml 8 | 9 | # IDE 10 | .idea 11 | .vscode 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | 17 | # Environments 18 | .env 19 | .venv 20 | env/ 21 | venv/ 22 | ENV/ 23 | 24 | # Airflow 25 | scheduler/airflow.cfg 26 | scheduler/airflow.db 27 | scheduler/unittests.cfg 28 | scheduler/logs/ 29 | scheduler/airflow-webserver.pid 30 | 31 | # Sphinx 32 | docs/_build/ 33 | 34 | # UI 35 | ui/node_modules/ 36 | ui/dist/ 37 | ui/.tmp/ 38 | ui/bower_components/ 39 | 40 | # macOS 41 | .DS_Store 42 | 43 | 44 | # Tests 45 | .coverage 46 | htmlcov/ 47 | .pytest_cache/ 48 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of DepC authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files 3 | # and it lists the copyright holders only. 4 | 5 | # Names should be added to this file as one of 6 | # Organization's name 7 | # Individual's name 8 | # Individual's name 9 | # See CONTRIBUTORS for the meaning of multiple email addresses. 10 | 11 | Nicolas Crocfer 12 | Anthony Olea 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing to DepC 2 | ==================== 3 | 4 | This project accepts contributions. In order to contribute, you should 5 | pay attention to a few things: 6 | 7 | 1. your code must follow the coding style rules 8 | 2. your code must be unit-tested 9 | 3. your code must be documented 10 | 4. your work must be signed (see below) 11 | 5. you may contribute through GitHub Pull Requests 12 | 13 | Submitting Modifications 14 | ------------------------ 15 | 16 | The contributions should be submitted through Github Pull Requests 17 | and follow the DCO which is defined below. 18 | 19 | Licensing for new files 20 | ----------------------- 21 | 22 | DepC is licensed under the 3-Clause BSD License license. Anything 23 | contributed DepC must be released under this license. 24 | 25 | When introducing a new file into the project, please make sure it has a 26 | copyright header making clear under which license it's being released. 27 | 28 | Developer Certificate of Origin (DCO) 29 | ------------------------------------- 30 | 31 | To improve tracking of contributions to this project we will use a 32 | process modeled on the modified DCO 1.1 and use a "sign-off" procedure 33 | on patches that are being emailed around or contributed in any other 34 | way. 35 | 36 | The sign-off is a simple line at the end of the explanation for the 37 | patch, which certifies that you wrote it or otherwise have the right 38 | to pass it on as an open-source patch. The rules are pretty simple: 39 | if you can certify the below: 40 | 41 | By making a contribution to this project, I certify that: 42 | 43 | (a) The contribution was created in whole or in part by me and I have 44 | the right to submit it under the open source license indicated in 45 | the file; or 46 | 47 | (b) The contribution is based upon previous work that, to the best of 48 | my knowledge, is covered under an appropriate open source License 49 | and I have the right under that license to submit that work with 50 | modifications, whether created in whole or in part by me, under 51 | the same open source license (unless I am permitted to submit 52 | under a different license), as indicated in the file; or 53 | 54 | (c) The contribution was provided directly to me by some other person 55 | who certified (a), (b) or (c) and I have not modified it. 56 | 57 | (d) The contribution is made free of any other party's intellectual 58 | property claims or rights. 59 | 60 | (e) I understand and agree that this project and the contribution are 61 | public and that a record of the contribution (including all 62 | personal information I submit with it, including my sign-off) is 63 | maintained indefinitely and may be redistributed consistent with 64 | this project or the open source license(s) involved. 65 | 66 | 67 | then you just add a line saying 68 | 69 | Signed-off-by: Random J Developer 70 | 71 | using your real name (sorry, no pseudonyms or anonymous contributions.) -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This is the official list of people who can contribute 2 | # (and typically have contributed) code to the DepC repository. 3 | # 4 | # Names should be added to this file only after verifying that 5 | # the individual or the individual's organization has agreed to 6 | # the appropriate CONTRIBUTING.md file. 7 | # 8 | # Names should be added to this file like so: 9 | # Individual's name 10 | # Individual's name 11 | 12 | Nicolas Crocfer 13 | Anthony Olea 14 | Paweł Kalemba 15 | Antoine Leblanc 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6 2 | 3 | ENTRYPOINT ["./docker-entrypoint.sh"] 4 | EXPOSE 5000 5 | 6 | RUN apt-get update && apt-get install -y libsnappy-dev 7 | 8 | # Working directory 9 | RUN mkdir -p /app 10 | WORKDIR /app 11 | 12 | # Apache Airflow 13 | ENV AIRFLOW_GPL_UNIDECODE yes 14 | 15 | # Install the Python requirements 16 | ADD requirements.txt /app/ 17 | RUN pip install --upgrade pip 18 | RUN pip install python-snappy==0.5.4 19 | RUN pip install -r requirements.txt 20 | 21 | # Copy the source files 22 | COPY . /app 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, OVH SAS. 2 | All rights reserved. 3 | Modified 3-Clause BSD 4 | 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | * Neither the name of OVH SAS nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY 19 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: ui tests docs scheduler consumer 2 | 3 | api: 4 | export FLASK_ENV=development && export FLASK_APP=manage:app && flask run 5 | 6 | ui: 7 | cd ui/ && grunt serve 8 | 9 | webserver: 10 | airflow webserver 11 | 12 | scheduler: 13 | airflow scheduler 14 | 15 | clean-docs: 16 | cd docs/ && make clean 17 | 18 | docs: clean-docs 19 | cd docs/ && make html 20 | 21 | tests: 22 | export DEPC_ENV=test && pytest tests/ -vv 23 | 24 | consumer: 25 | python consumer/main.py 26 | -------------------------------------------------------------------------------- /consumer/definitions.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": { 3 | "label": {"type": "string", "pattern": "^[A-Z]+[a-zA-Z0-9]*$"}, 4 | "name": {"type": "string", "pattern": "^[!#-&(-[\\]-_a-~]+$"}, 5 | "from": {"type": "integer", "minimum": 0}, 6 | "to": {"type": "integer", "minimum": 1}, 7 | "node": { 8 | "type": "object", 9 | "properties": { 10 | "label": { "$ref": "#/definitions/label" }, 11 | "name": { "$ref": "#/definitions/name" }, 12 | "props": { 13 | "type": "object", 14 | "properties": { 15 | "from": { "$ref": "#/definitions/from" }, 16 | "to": { "$ref": "#/definitions/to" } 17 | }, 18 | "additionalProperties": false 19 | } 20 | }, 21 | "additionalProperties": false, 22 | "required": ["label", "name"] 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /consumer/main.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | 3 | import os 4 | import sys 5 | from pathlib import Path 6 | 7 | sys.path.append(os.getenv("DEPC_HOME", str(Path(__file__).resolve().parents[1]))) 8 | 9 | 10 | if __name__ == "__main__": 11 | from consumer.kafka_consumer.utils import ( 12 | CONSUMER_CONFIG, 13 | KAFKA_CONFIG, 14 | NEO4J_CONFIG, 15 | ) 16 | from consumer.kafka_consumer import run_consumer 17 | 18 | run_consumer(CONSUMER_CONFIG, KAFKA_CONFIG, NEO4J_CONFIG) 19 | -------------------------------------------------------------------------------- /consumer/schemas.json: -------------------------------------------------------------------------------- 1 | { 2 | "flat": { 3 | "type": "object", 4 | "properties": { 5 | "source_label": { "$ref": "#/definitions/label" }, 6 | "source_name": { "$ref": "#/definitions/name" }, 7 | "source_from": { "$ref": "#/definitions/from" }, 8 | "source_to": { "$ref": "#/definitions/to" }, 9 | "target_label": { "$ref": "#/definitions/label" }, 10 | "target_name": { "$ref": "#/definitions/name" }, 11 | "target_from": { "$ref": "#/definitions/from" }, 12 | "target_to": { "$ref": "#/definitions/to" }, 13 | "rel_from": { "$ref": "#/definitions/from" }, 14 | "rel_to": { "$ref": "#/definitions/to" } 15 | }, 16 | "additionalProperties": false, 17 | "dependencies": {"target_label": ["target_name"]}, 18 | "required": ["source_label", "source_name"] 19 | }, 20 | "nested": { 21 | "type": "object", 22 | "properties": { 23 | "source": { "$ref": "#/definitions/node" }, 24 | "target": { "$ref": "#/definitions/node" }, 25 | "rel": { 26 | "type": "object", 27 | "properties": { 28 | "from": { "$ref": "#/definitions/from" }, 29 | "to": { "$ref": "#/definitions/to" } 30 | }, 31 | "additionalProperties": false 32 | } 33 | }, 34 | "additionalProperties": false, 35 | "dependencies": {"target": ["rel"]}, 36 | "required": ["source"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /depc.example.yml: -------------------------------------------------------------------------------- 1 | SQLALCHEMY_DATABASE_URI: postgresql://user:pass@host/database?connect_timeout=5&keepalives=1&keepalives_idle=5&keepalives_interval=5&keepalives_count=2 2 | 3 | SECRET: mysecretkey 4 | 5 | DB_ENCRYPTION_KEY: mydbencryptionkey 6 | 7 | BASE_UI_URL: http://127.0.0.1/ 8 | 9 | FLOAT_DECIMAL: 3 10 | 11 | MAX_WORST_ITEMS: 15 12 | 13 | FORCE_INSECURE_ADMIN: false 14 | 15 | NEO4J: 16 | url: http://127.0.0.1:7474 17 | uri: bolt://127.0.0.1:7687 18 | username: neo4j 19 | password: p4ssw0rd 20 | # Optional setting 21 | # encrypted: False 22 | 23 | WARP10: 24 | url: https://example.com/api/v0 25 | rotoken: token 26 | wtoken: token 27 | 28 | WARP10_CACHE: 29 | url: https://example.com/api/v0 30 | rotoken: token 31 | wtoken: token 32 | 33 | REDIS_CACHE: 34 | url: redis://localhost/3 35 | 36 | # Required to run DepC DAGs with Airflow 37 | REDIS_SCHEDULER_CACHE: 38 | url: redis://localhost/4 39 | 40 | BEAMIUM: 41 | source-dir: /opt/beamium/sources 42 | 43 | # Please read the Loguru official documentation for further details about the logging format 44 | LOGGING: 45 | level: INFO 46 | format: {time:YYYY-MM-DD HH:mm:ss.SSS} | {level:<8} | {name}:{function}:{line} - {message} 47 | # Enable the Graylog Extended Log Format 48 | gelf: false 49 | 50 | # Configure the Kafka consumer to populate the Neo4j graph database 51 | # The consumer uses SASL/PLAIN authentication 52 | # One topic per team 53 | CONSUMER: 54 | kafka: 55 | hosts: localhost:9093 56 | batch_size: 10 57 | topics: [depc.my_topic] 58 | username: depc.consumer 59 | password: p4ssw0rd 60 | client_id: depc.consumer 61 | group_id: depc.consumer.depc_consumer_group 62 | heartbeat: 63 | delay: 5 64 | 65 | # Optional: exclude a specific team or rule from the regular QoS compute method 66 | # This variable needs a list of UUIDs used in the database 67 | # The compute for these IDs will be based only on the data points sent by the source 68 | EXCLUDE_FROM_AUTO_FILL: 69 | - 123e4567-e89b-12d3-a456-426655440000 70 | -------------------------------------------------------------------------------- /depc/apiv1/configs.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify, request 2 | from flask_login import login_required 3 | from werkzeug.exceptions import abort 4 | 5 | from depc.apiv1 import api 6 | from depc.controllers.configs import ConfigController 7 | from depc.users import TeamPermission 8 | 9 | 10 | @api.route("/teams//configs/current") 11 | @login_required 12 | def get_current_config(team_id): 13 | """ 14 | 15 | .. :quickref: GET; Lorem ipsum.""" 16 | if not TeamPermission.is_user(team_id): 17 | abort(403) 18 | 19 | config = ConfigController.get_current_config(team_id=team_id) 20 | return jsonify(config), 200 21 | 22 | 23 | @api.route( 24 | "/teams//configs", 25 | methods=["POST"], 26 | request_schema=("v1_config", "config_input"), 27 | ) 28 | @login_required 29 | def put_config(team_id): 30 | """ 31 | 32 | .. :quickref: POST; Lorem ipsum.""" 33 | if not TeamPermission.is_manager(team_id): 34 | abort(403) 35 | 36 | payload = request.get_json(force=True) 37 | current_conf = ConfigController.create({"team_id": team_id, "data": payload}) 38 | return jsonify(current_conf), 200 39 | 40 | 41 | @api.route("/teams//configs") 42 | @login_required 43 | def list_configs(team_id): 44 | """ 45 | 46 | .. :quickref: GET; Lorem ipsum.""" 47 | if not TeamPermission.is_user(team_id): 48 | abort(403) 49 | 50 | configs = ConfigController.list( 51 | filters={"Config": {"team_id": team_id}}, order_by="updated_at", reverse=True 52 | ) 53 | return jsonify(configs), 200 54 | 55 | 56 | @api.route("/teams//configs/") 57 | @login_required 58 | def get_config(team_id, config_id): 59 | """ 60 | 61 | .. :quickref: GET; Lorem ipsum.""" 62 | if not TeamPermission.is_user(team_id): 63 | abort(403) 64 | 65 | config = ConfigController.get( 66 | filters={"Config": {"team_id": team_id, "id": config_id}} 67 | ) 68 | if not config: 69 | abort(404) 70 | 71 | return jsonify(config), 200 72 | 73 | 74 | @api.route("/teams//configs//apply", methods=["PUT"]) 75 | @login_required 76 | def revert_config(team_id, config_id): 77 | """ 78 | 79 | .. :quickref: PUT; Lorem ipsum.""" 80 | if not TeamPermission.is_manager(team_id): 81 | abort(403) 82 | 83 | config = ConfigController.revert_config(team_id=team_id, config_id=config_id) 84 | if not config: 85 | abort(404) 86 | return jsonify(config), 200 87 | -------------------------------------------------------------------------------- /depc/apiv1/errors.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify, make_response 2 | from jsonschema.exceptions import ValidationError 3 | 4 | from depc.apiv1 import api 5 | from depc.controllers import ( 6 | NotFoundError, 7 | AlreadyExistError, 8 | RequirementsNotSatisfiedError, 9 | IntegrityError, 10 | ) 11 | from depc.sources.exceptions import BadConfigurationException 12 | 13 | 14 | def format_controller_error(e): 15 | content = {"message": str(e)} 16 | return jsonify(content) 17 | 18 | 19 | def format_error(code, message): 20 | content = {"message": message} 21 | # We cannot put that in an after_request because they are not triggered 22 | # when returning a 500 23 | response = make_response(jsonify(content), code) 24 | return response 25 | 26 | 27 | @api.app_errorhandler(NotFoundError) 28 | def not_found_error_handler(e): 29 | return format_error(404, str(e)) 30 | 31 | 32 | @api.app_errorhandler(ValidationError) 33 | def validation_error_handler(e): 34 | return format_error(400, e.message) 35 | 36 | 37 | @api.app_errorhandler(IntegrityError) 38 | def integrity_error_handler(e): 39 | return format_error(400, str(e)) 40 | 41 | 42 | @api.app_errorhandler(NotImplementedError) 43 | def not_implemented_error_handler(e): 44 | return format_error(400, str(e)) 45 | 46 | 47 | @api.app_errorhandler(BadConfigurationException) 48 | def bad_configuration_error_handler(e): 49 | return format_error(400, str(e)) 50 | 51 | 52 | @api.app_errorhandler(AlreadyExistError) 53 | def already_exists_error_handler(e): 54 | return format_controller_error(e), 409 55 | 56 | 57 | @api.app_errorhandler(RequirementsNotSatisfiedError) 58 | def requirements_error_handler(e): 59 | return format_controller_error(e), 409 60 | 61 | 62 | @api.app_errorhandler(400) 63 | def bad_request_handler(e): 64 | return format_error(400, "The server did not understand your request") 65 | 66 | 67 | @api.app_errorhandler(401) 68 | def unauthorized_handler(e): 69 | return format_error(401, "Could not verify your access level for that URL") 70 | 71 | 72 | @api.app_errorhandler(403) 73 | def forbidden_handler(e): 74 | return format_error( 75 | 403, "You do not have the required permissions for that resource" 76 | ) 77 | 78 | 79 | @api.app_errorhandler(404) 80 | def not_found_handler(e): 81 | return format_error(404, "The requested resource could not be found") 82 | 83 | 84 | @api.app_errorhandler(405) 85 | def method_not_allowed_handler(e): 86 | return format_error(405, "The method is not allowed for the requested URL") 87 | 88 | 89 | @api.app_errorhandler(500) 90 | def internal_server_error_handler(e): 91 | return format_error( 92 | 500, 93 | "The server has either erred or is incapable " 94 | "of performing the requested operation", 95 | ) 96 | -------------------------------------------------------------------------------- /depc/apiv1/qos.py: -------------------------------------------------------------------------------- 1 | import arrow 2 | from flask import abort, jsonify, request 3 | from flask_login import login_required 4 | 5 | from depc.apiv1 import api 6 | from depc.controllers.qos import QosController 7 | from depc.controllers.worst import WorstController 8 | from depc.users import TeamPermission 9 | 10 | 11 | @api.route("/teams//qos") 12 | @login_required 13 | def get_team_qos(team_id): 14 | """ 15 | 16 | .. :quickref: GET; Lorem ipsum.""" 17 | if not TeamPermission.is_user(team_id): 18 | abort(403) 19 | 20 | label = request.args.get("label", None) 21 | name = request.args.get("name", None) 22 | start = request.args.get("start", None) 23 | end = request.args.get("end", None) 24 | 25 | if name: 26 | data = QosController.get_team_item_qos(team_id, label, name, start, end) 27 | else: 28 | data = QosController.get_team_qos(team_id, label, start, end) 29 | 30 | return jsonify(data) 31 | 32 | 33 | @api.route("/teams//qos/worst") 34 | @login_required 35 | def get_team_worst_qos(team_id): 36 | """ 37 | 38 | .. :quickref: GET; Lorem ipsum.""" 39 | if not TeamPermission.is_user(team_id): 40 | abort(403) 41 | 42 | label = request.args.get("label", None) 43 | date = request.args.get("date", arrow.now().shift(days=-1).format("YYYY-MM-DD")) 44 | 45 | return jsonify(WorstController.get_daily_worst_items(team_id, label, date)) 46 | -------------------------------------------------------------------------------- /depc/apiv1/statistics.py: -------------------------------------------------------------------------------- 1 | from flask import abort, jsonify, request 2 | from flask_login import login_required 3 | 4 | from depc.apiv1 import api 5 | from depc.controllers.statistics import StatisticsController 6 | from depc.users import TeamPermission 7 | 8 | 9 | @api.route("/teams//statistics") 10 | @login_required 11 | def get_team_statistics(team_id): 12 | """ 13 | 14 | .. :quickref: GET; Lorem ipsum.""" 15 | if not TeamPermission.is_user(team_id): 16 | abort(403) 17 | 18 | return ( 19 | jsonify( 20 | StatisticsController.get_team_statistics( 21 | team_id=team_id, 22 | start=request.args.get("start", None), 23 | end=request.args.get("end", None), 24 | label=request.args.get("label", None), 25 | type=request.args.get("type", None), 26 | sort=request.args.get("sort", "label"), 27 | ) 28 | ), 29 | 200, 30 | ) 31 | -------------------------------------------------------------------------------- /depc/apiv1/users.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from flask_login import login_required 3 | 4 | from depc.apiv1 import api, format_object 5 | from depc.controllers.users import UserController 6 | 7 | VISIBLE = ["name", "grants"] 8 | 9 | 10 | def format_user(user): 11 | s = format_object(user, VISIBLE) 12 | return s 13 | 14 | 15 | @api.route("/users") 16 | @login_required 17 | def list_users(): 18 | """ 19 | 20 | .. :quickref: GET; Lorem ipsum.""" 21 | users = UserController.list() 22 | return jsonify({"users": [format_user(u) for u in users]}), 200 23 | 24 | 25 | @api.route("/users/me") 26 | @login_required 27 | def me(): 28 | """ 29 | 30 | .. :quickref: GET; Lorem ipsum.""" 31 | user = UserController.get_current_user() 32 | return jsonify(format_user(user)), 200 33 | -------------------------------------------------------------------------------- /depc/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/depc/commands/__init__.py -------------------------------------------------------------------------------- /depc/commands/config.py: -------------------------------------------------------------------------------- 1 | import click 2 | import yaml 3 | from flask import current_app as app 4 | from flask.cli import AppGroup 5 | from flask.cli import with_appcontext 6 | 7 | config_cli = AppGroup("config", help="Manage configuration.") 8 | 9 | 10 | @config_cli.command("show") 11 | @with_appcontext 12 | def show_config(): 13 | """Show the current configuration.""" 14 | click.echo(yaml.dump(dict(app.config))) 15 | -------------------------------------------------------------------------------- /depc/commands/key.py: -------------------------------------------------------------------------------- 1 | import click 2 | from flask import current_app as app 3 | from flask.cli import AppGroup 4 | from flask.cli import with_appcontext 5 | 6 | from depc.extensions.encrypted_dict import FlaskEncryptedDict 7 | 8 | key_cli = AppGroup("key", help="Manage database key.") 9 | 10 | 11 | @key_cli.command("generate") 12 | def generate_key(): 13 | """Generate a 256-bit hex key to encrypt database.""" 14 | click.echo(FlaskEncryptedDict.generate_key()) 15 | 16 | 17 | @key_cli.command("change") 18 | @with_appcontext 19 | @click.option("--old-key", default=None, required=True, type=str) 20 | @click.option("--new-key", default=None, required=True, type=str) 21 | def change_key(old_key, new_key): 22 | """Change the 256-bit hex key to encrypt database.""" 23 | if old_key is None: 24 | old_key = app.config.get("DB_ENCRYPTION_KEY") 25 | 26 | FlaskEncryptedDict.change_key(old_key, new_key) 27 | click.echo("Database key has been changed") 28 | click.echo("Add this key to depc.{env}.yml as DB_ENCRYPTION_KEY") 29 | click.echo(new_key) 30 | -------------------------------------------------------------------------------- /depc/commands/user.py: -------------------------------------------------------------------------------- 1 | import click 2 | from flask.cli import AppGroup 3 | from flask.cli import with_appcontext 4 | 5 | from depc.controllers import NotFoundError 6 | from depc.controllers.users import UserController 7 | 8 | user_cli = AppGroup("user", help="Manage users.") 9 | 10 | 11 | @user_cli.command("create") 12 | @with_appcontext 13 | @click.argument("name", default=None, required=True, type=str) 14 | @click.option("--admin", default=False, required=False, type=bool, is_flag=True) 15 | def create_user(name, admin): 16 | """Create a new user.""" 17 | is_admin = bool(admin) 18 | try: 19 | user = UserController._get(filters={"User": {"name": name}}) 20 | click.echo( 21 | "User {name} already exists with id: {id}".format(name=name, id=user.id) 22 | ) 23 | except NotFoundError: 24 | click.confirm( 25 | "User {name} will be created with admin = {admin}".format( 26 | name=name, admin=is_admin 27 | ), 28 | abort=True, 29 | ) 30 | UserController.create({"name": name, "active": True, "admin": is_admin}) 31 | -------------------------------------------------------------------------------- /depc/context.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from pathlib import Path 4 | 5 | from depc import BASE_DIR 6 | from depc.extensions import ( 7 | admin, 8 | cors, 9 | db, 10 | jsonschema, 11 | migrate, 12 | flask_encrypted_dict, 13 | login_manager, 14 | redis, 15 | redis_scheduler, 16 | ) 17 | from depc.logs import setup_loggers 18 | 19 | 20 | class Config: 21 | BASE_UI_URL = "http://127.0.0.1/" 22 | SECRET_KEY = os.environ.get("SECRET_KEY") or "mysecret" 23 | JSON_AS_ASCII = False 24 | DEBUG = False 25 | LOGGERS = {} 26 | LOGGING = {"level": "DEBUG"} 27 | SQLALCHEMY_DATABASE_URI = "sqlite://" 28 | NEO4J = { 29 | "url": "http://127.0.0.1:7474", 30 | "uri": "bolt://127.0.0.1:7687", 31 | "username": "neo4j", 32 | "password": "neo4j", 33 | "encrypted": False, 34 | } 35 | CONSUMER = { 36 | "kafka": { 37 | "hosts": "localhost:9093", 38 | "batch_size": 10, 39 | "topics": ["depc.my_topic"], 40 | "username": "depc.consumer", 41 | "password": "p4ssw0rd", 42 | "client_id": "depc.consumer", 43 | "group_id": "depc.consumer.depc_consumer_group", 44 | } 45 | } 46 | JSONSCHEMA_DIR = str(Path(BASE_DIR) / "schemas") 47 | STATIC_DIR = str(Path(BASE_DIR) / "static") 48 | SQLALCHEMY_TRACK_MODIFICATIONS = True 49 | MAX_WORST_ITEMS = 15 50 | EXCLUDE_FROM_AUTO_FILL = [] 51 | 52 | @staticmethod 53 | def init_app(app): 54 | setup_loggers(app) 55 | with app.app_context(): 56 | db.init_app(app) 57 | migrate.init_app(app, db) 58 | flask_encrypted_dict.init_app(app) 59 | jsonschema.init_app(app) 60 | redis.init_app(app) 61 | redis_scheduler.init_app(app, config_prefix="REDIS_SCHEDULER_CACHE") 62 | login_manager.init_app(app) 63 | cors.init_app(app) 64 | 65 | 66 | class TestingConfig(Config): 67 | DEBUG = True 68 | NEO4J = { 69 | **Config.NEO4J, 70 | "password": "foobar", 71 | } 72 | 73 | @staticmethod 74 | def init_app(app): 75 | Config.init_app(app) 76 | admin.init_app(app) 77 | 78 | 79 | class DevelopmentConfig(Config): 80 | DEBUG = True 81 | 82 | @staticmethod 83 | def init_app(app): 84 | Config.init_app(app) 85 | admin.init_app(app) 86 | 87 | 88 | class ProductionConfig(Config): 89 | DEBUG = False 90 | 91 | @staticmethod 92 | def init_app(app): 93 | Config.init_app(app) 94 | admin.init_app(app) 95 | 96 | 97 | class SnakeoilConfig(Config): 98 | TESTING = True 99 | DEBUG = True 100 | 101 | @staticmethod 102 | def init_app(app): 103 | Config.init_app(app) 104 | 105 | 106 | # Aliases 107 | DevConfig = DevelopmentConfig 108 | ProdConfig = ProductionConfig 109 | TestConfig = TestingConfig 110 | -------------------------------------------------------------------------------- /depc/controllers/checks.py: -------------------------------------------------------------------------------- 1 | from depc.controllers import ( 2 | Controller, 3 | NotFoundError, 4 | AlreadyExistError, 5 | IntegrityError, 6 | ) 7 | from depc.controllers.sources import SourceController 8 | from depc.extensions import db 9 | from depc.models.checks import Check 10 | from depc.models.rules import Rule 11 | from depc.models.sources import Source 12 | from depc.models.teams import Team 13 | 14 | 15 | class CheckController(Controller): 16 | 17 | model_cls = Check 18 | 19 | @classmethod 20 | def list_team_checks(cls, team_id): 21 | from depc.controllers.teams import TeamController 22 | 23 | _ = TeamController.get({"Team": {"id": team_id}}) 24 | 25 | checks = ( 26 | db.session.query(Check) 27 | .join(Source, Source.id == Check.source_id) 28 | .join(Team, Team.id == Source.team_id) 29 | .filter(Team.id == team_id) 30 | .all() 31 | ) 32 | return [cls.resource_to_dict(c) for c in checks] 33 | 34 | @classmethod 35 | def _join_to(cls, query, object_class): 36 | if object_class == Rule: 37 | return query.join(Check.rules) 38 | return super(CheckController, cls)._join_to(query, object_class) 39 | 40 | @classmethod 41 | def before_data_load(cls, data): 42 | if "source_id" in data: 43 | try: 44 | SourceController.get(filters={"Source": {"id": data["source_id"]}}) 45 | except NotFoundError: 46 | raise NotFoundError("Source {} not found".format(data["source_id"])) 47 | 48 | @classmethod 49 | def ensure_check(cls, obj): 50 | name = obj.name 51 | 52 | # Name surrounded by quotes are prohibited 53 | if name.startswith(('"', "'")) or name.endswith(('"', "'")): 54 | raise IntegrityError("The check name cannot begin or end with a quote") 55 | 56 | # Ensure that the check does not exist in another source 57 | checks = cls._list(filters={"Check": {"name": name}}) 58 | source = SourceController._get(filters={"Source": {"id": obj.source_id}}) 59 | 60 | for check in checks: 61 | if check.id != obj.id and check.source.team_id == source.team_id: 62 | raise AlreadyExistError( 63 | "The check {name} already exists.", {"name": name} 64 | ) 65 | 66 | # Ensure the type field 67 | if ":" in obj.parameters["threshold"] and obj.type != "Interval": 68 | raise IntegrityError( 69 | "Threshold {} must be flagged as interval".format( 70 | obj.parameters["threshold"] 71 | ) 72 | ) 73 | 74 | @classmethod 75 | def before_create(cls, obj): 76 | cls.ensure_check(obj) 77 | 78 | @classmethod 79 | def before_update(cls, obj): 80 | cls.ensure_check(obj) 81 | -------------------------------------------------------------------------------- /depc/controllers/grants.py: -------------------------------------------------------------------------------- 1 | from depc.controllers import Controller 2 | from depc.models.users import Grant 3 | 4 | 5 | class GrantController(Controller): 6 | 7 | model_cls = Grant 8 | 9 | @classmethod 10 | def resource_to_dict(cls, obj, blacklist=False): 11 | return {"user": obj.user.name, "role": obj.role.value} 12 | -------------------------------------------------------------------------------- /depc/controllers/news.py: -------------------------------------------------------------------------------- 1 | from flask_login import current_user 2 | 3 | from depc.controllers import Controller 4 | from depc.extensions import db 5 | from depc.models.news import News 6 | 7 | 8 | class NewsController(Controller): 9 | 10 | model_cls = News 11 | 12 | @classmethod 13 | def list(cls, limit=None, unread=False): 14 | news = cls._list(order_by="created_at", reverse=True) 15 | 16 | # Select news unread by current user 17 | if unread: 18 | news = [n for n in news if n not in current_user.news] 19 | 20 | # If limit is specified 21 | try: 22 | if limit and int(limit) <= len(news): 23 | news = news[: int(limit)] 24 | except ValueError: 25 | pass 26 | 27 | return [cls.resource_to_dict(n) for n in news] 28 | 29 | @classmethod 30 | def get(cls, *args, **kwargs): 31 | news = cls._get(*args, **kwargs) 32 | 33 | if current_user not in news.users: 34 | news.users.append(current_user) 35 | db.session.add(news) 36 | db.session.commit() 37 | 38 | return cls.resource_to_dict(news) 39 | 40 | @classmethod 41 | def clear(cls): 42 | current_user.news = cls._list() 43 | db.session.add(current_user) 44 | db.session.commit() 45 | return {} 46 | -------------------------------------------------------------------------------- /depc/controllers/qos.py: -------------------------------------------------------------------------------- 1 | from depc.controllers import Controller 2 | from depc.extensions import redis 3 | from depc.queries import ( 4 | QOS_ITEM_PER_TEAM_LABEL, 5 | QOS_WORST_ITEM_PER_LABEL, 6 | QOS_PER_TEAM, 7 | QOS_FILTERED_BY_LABEL, 8 | ) 9 | from depc.utils.warp10 import Warp10Client, _transform_warp10_values 10 | 11 | 12 | class QosController(Controller): 13 | @classmethod 14 | @redis.cache(period=redis.seconds_until_midnight) 15 | def get_team_qos(cls, team_id, label, start, end): 16 | cls.check_valid_period(start, end) 17 | client = Warp10Client() 18 | 19 | # Default values 20 | params = {"team": team_id} 21 | 22 | script = QOS_PER_TEAM 23 | if label: 24 | script = QOS_FILTERED_BY_LABEL 25 | params.update({"name": label}) 26 | 27 | client.generate_script(start=start, end=end, script=script, extra_params=params) 28 | resp = client.execute() 29 | 30 | # Return list of datapoints 31 | if label: 32 | try: 33 | return _transform_warp10_values(resp[0][0]["v"]) 34 | except IndexError: 35 | return {} 36 | 37 | # Order by Label 38 | data = {} 39 | 40 | for metrics in resp[0]: 41 | label = metrics["l"]["name"] 42 | 43 | if label not in data: 44 | data[label] = {} 45 | data[label] = _transform_warp10_values(metrics["v"]) 46 | 47 | return data 48 | 49 | @classmethod 50 | @redis.cache(period=redis.seconds_until_midnight) 51 | def get_team_item_qos(cls, team, label, name, start, end): 52 | cls.check_valid_period(start, end) 53 | client = Warp10Client() 54 | 55 | client.generate_script( 56 | start=start, 57 | end=end, 58 | script=QOS_ITEM_PER_TEAM_LABEL, 59 | extra_params={"team": team, "label": label, "name": name}, 60 | ) 61 | resp = client.execute() 62 | 63 | # No result 64 | if not resp[0]: 65 | return {} 66 | 67 | values = _transform_warp10_values(resp[0][0]["v"]) 68 | return values 69 | 70 | @classmethod 71 | @redis.cache(period=redis.seconds_until_midnight) 72 | def get_label_worst_items(cls, team, label, start, end, count): 73 | cls.check_valid_period(start, end) 74 | client = Warp10Client() 75 | 76 | client.generate_script( 77 | start=start, 78 | end=end, 79 | script=QOS_WORST_ITEM_PER_LABEL, 80 | extra_params={"team": team, "label": label, "count": str(count)}, 81 | ) 82 | 83 | resp = client.execute() 84 | 85 | # No result 86 | if not resp[0]: 87 | return {} 88 | 89 | data = [] 90 | for metric in resp[0]: 91 | data.append({"name": metric["l"]["name"], "qos": metric["v"][0][1]}) 92 | 93 | return data 94 | -------------------------------------------------------------------------------- /depc/controllers/sources.py: -------------------------------------------------------------------------------- 1 | from flask_login import current_user 2 | 3 | from depc.controllers import Controller, RequirementsNotSatisfiedError 4 | from depc.models.sources import Source 5 | from depc.sources import BaseSource 6 | 7 | 8 | class SourceController(Controller): 9 | 10 | model_cls = Source 11 | 12 | @classmethod 13 | def get(cls, *args, **kwargs): 14 | return super(SourceController, cls).get(*args, **kwargs) 15 | 16 | @classmethod 17 | def update(cls, *args, **kwargs): 18 | return super(SourceController, cls).update(*args, **kwargs) 19 | 20 | @classmethod 21 | def delete(cls, *args, **kwargs): 22 | return super(SourceController, cls).delete(*args, **kwargs) 23 | 24 | @classmethod 25 | def resource_to_dict(cls, obj, blacklist=False): 26 | from ..controllers.checks import CheckController 27 | 28 | d = super().resource_to_dict(obj, blacklist=False) 29 | d["checks"] = [CheckController.resource_to_dict(c) for c in obj.checks] 30 | return d 31 | 32 | @classmethod 33 | def ensure_plugin(cls, obj): 34 | BaseSource.validate_source_config(obj.plugin, obj.configuration) 35 | 36 | @classmethod 37 | def before_create(cls, obj): 38 | obj.manager = current_user 39 | cls.ensure_plugin(obj) 40 | 41 | @classmethod 42 | def before_update(cls, obj): 43 | cls.ensure_plugin(obj) 44 | 45 | @classmethod 46 | def before_delete(cls, obj): 47 | if obj.checks: 48 | msg = "Source {0} contains {1} check{2}, please remove it before.".format( 49 | obj.name, len(obj.checks), "s" if len(obj.checks) > 1 else "" 50 | ) 51 | raise RequirementsNotSatisfiedError(msg) 52 | -------------------------------------------------------------------------------- /depc/controllers/statistics.py: -------------------------------------------------------------------------------- 1 | from depc.controllers import Controller 2 | from depc.extensions import redis 3 | from depc.queries import STATISTICS_SCRIPT 4 | from depc.utils.warp10 import Warp10Client 5 | 6 | 7 | class StatisticsController(Controller): 8 | @classmethod 9 | @redis.cache(period=redis.seconds_until_midnight) 10 | def get_team_statistics( 11 | cls, team_id, start, end, label=None, type=None, sort="label" 12 | ): 13 | cls.check_valid_period(start, end) 14 | client = Warp10Client() 15 | 16 | filter_str = " " 17 | if label: 18 | filter_str += "'label' '{0}' ".format(label) 19 | if type: 20 | filter_str += "'type' '{0}' ".format(type) 21 | 22 | client.generate_script( 23 | start=start, 24 | end=end, 25 | script=STATISTICS_SCRIPT, 26 | extra_params={"team": team_id, "filter": filter_str}, 27 | ) 28 | resp = client.execute() 29 | 30 | # No result 31 | if not resp[0]: 32 | return {} 33 | 34 | # Sort the results 35 | sort_choices = ["label", "type"] 36 | main_sort = sort_choices.pop( 37 | sort_choices.index(sort if sort in sort_choices else "label") 38 | ) 39 | second_sort = sort_choices.pop() 40 | 41 | result = {} 42 | 43 | # We want to have some statistics for all labels 44 | # (ex: total number of nodes for the team) 45 | if sort == "label": 46 | result["*"] = {} 47 | 48 | for statistic in resp[0]: 49 | main = statistic["l"][main_sort] 50 | second = statistic["l"][second_sort] 51 | 52 | if main not in result: 53 | result[main] = {} 54 | 55 | if second not in result[main]: 56 | result[main][second] = {} 57 | 58 | # Total of stats for all labels 59 | if sort == "label": 60 | if second not in result["*"]: 61 | result["*"][second] = {} 62 | 63 | for v in statistic["v"]: 64 | ts = int(v[0] / 1000000) # ms to s 65 | result[main][second][ts] = v[1] 66 | 67 | # Increment the total 68 | if sort == "label": 69 | if ts not in result["*"][second]: 70 | result["*"][second][ts] = 0 71 | result["*"][second][ts] += v[1] 72 | 73 | return result 74 | -------------------------------------------------------------------------------- /depc/controllers/users.py: -------------------------------------------------------------------------------- 1 | import werkzeug.security 2 | from flask_login import current_user 3 | 4 | from depc.controllers import Controller, AlreadyExistError 5 | from depc.extensions import db 6 | from depc.models.users import User 7 | 8 | 9 | class UserController(Controller): 10 | 11 | model_cls = User 12 | 13 | @classmethod 14 | def get_current_user(cls): 15 | user = current_user.to_dict() 16 | user["grants"] = {} 17 | 18 | for grant in current_user.grants: 19 | user["grants"][grant.team.name] = grant.role.name 20 | 21 | return user 22 | 23 | @classmethod 24 | def before_data_load(cls, data): 25 | if "password" in data: 26 | data["password"] = werkzeug.security.generate_password_hash( 27 | data["password"] 28 | ) 29 | 30 | @classmethod 31 | def handle_integrity_error(cls, obj, error): 32 | db.session.rollback() 33 | if User.query.filter_by(username=obj.username).all(): 34 | raise AlreadyExistError( 35 | "The user {username} already exists.", {"username": obj.username} 36 | ) 37 | -------------------------------------------------------------------------------- /depc/controllers/variables.py: -------------------------------------------------------------------------------- 1 | from depc.controllers import Controller, NotFoundError 2 | from depc.models.sources import Source 3 | from depc.models.teams import Team 4 | from depc.models.variables import Variable 5 | 6 | 7 | class VariableController(Controller): 8 | 9 | model_cls = Variable 10 | 11 | @classmethod 12 | def _join_to(cls, query, object_class): 13 | if object_class == Source: 14 | return query.join(Source.variables) 15 | if object_class == Team: 16 | return query.join(Team.variables) 17 | return super(VariableController, cls)._join_to(query, object_class) 18 | 19 | @classmethod 20 | def _not_found(cls): 21 | raise NotFoundError("Could not find resource") 22 | 23 | @classmethod 24 | def _check_object(cls, obj): 25 | from ..controllers.checks import CheckController 26 | from ..controllers.rules import RuleController 27 | from ..controllers.sources import SourceController 28 | 29 | # Team id is always mandatory 30 | team_id = obj.team_id 31 | 32 | # Check the rule 33 | if obj.rule_id: 34 | rule = RuleController.get( 35 | filters={"Rule": {"id": obj.rule_id, "team_id": team_id}} 36 | ) 37 | if not rule: 38 | cls._not_found() 39 | 40 | # Check the source 41 | if obj.source_id: 42 | source = SourceController.get( 43 | filters={"Source": {"id": obj.source_id, "team_id": team_id}} 44 | ) 45 | if not source: 46 | cls._not_found() 47 | 48 | # Check the check (haha!) 49 | if obj.source_id and obj.check_id: 50 | source = CheckController.get( 51 | filters={"Check": {"id": obj.check_id, "source_id": obj.source_id}} 52 | ) 53 | if not source: 54 | cls._not_found() 55 | 56 | @classmethod 57 | def resource_to_dict(cls, obj, blacklist=False): 58 | d = super().resource_to_dict(obj, blacklist=False) 59 | d["expression"] = obj.expression 60 | return d 61 | -------------------------------------------------------------------------------- /depc/controllers/worst.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from depc.controllers import Controller 4 | from depc.controllers import NotFoundError, RequirementsNotSatisfiedError 5 | from depc.extensions import redis 6 | from depc.models.worst import Periods 7 | from depc.models.worst import Worst 8 | 9 | 10 | def validate_date(date_text): 11 | try: 12 | datetime.datetime.strptime(date_text, "%Y-%m-%d") 13 | except ValueError: 14 | raise RequirementsNotSatisfiedError( 15 | "Invalid date format (expected: YYYY-MM-DD)" 16 | ) 17 | 18 | 19 | class WorstController(Controller): 20 | 21 | model_cls = Worst 22 | 23 | @classmethod 24 | @redis.cache(period=redis.seconds_until_midnight) 25 | def get_daily_worst_items(cls, team_id, label, date): 26 | validate_date(date) 27 | try: 28 | worst_items = WorstController.get( 29 | filters={ 30 | "Worst": { 31 | "team_id": team_id, 32 | "label": label, 33 | "period": Periods.daily, 34 | "date": date, 35 | } 36 | } 37 | ) 38 | 39 | return [ 40 | {"name": name, "qos": qos} for name, qos in worst_items["data"].items() 41 | ] 42 | except NotFoundError: 43 | return [] 44 | -------------------------------------------------------------------------------- /depc/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_admin import Admin 2 | from flask_admin.base import AdminIndexView 3 | from flask_cors import CORS 4 | from flask_jsonschema import JsonSchema 5 | from flask_login import LoginManager 6 | from flask_migrate import Migrate 7 | from flask_sqlalchemy import SQLAlchemy 8 | from sqlalchemy import event, exc, select 9 | from sqlalchemy.engine import Engine 10 | 11 | from depc.extensions.encrypted_dict import FlaskEncryptedDict 12 | from depc.extensions.flask_redis_cache import FlaskRedisCache 13 | 14 | admin = Admin(index_view=AdminIndexView()) 15 | db = SQLAlchemy(session_options={"autoflush": False}) 16 | migrate = Migrate() 17 | cors = CORS(send_wildcard=True) 18 | jsonschema = JsonSchema() 19 | flask_encrypted_dict = FlaskEncryptedDict() 20 | login_manager = LoginManager() 21 | redis = FlaskRedisCache() 22 | redis_scheduler = FlaskRedisCache() 23 | 24 | 25 | @login_manager.request_loader 26 | def load_user_from_request(request): 27 | from depc.models.users import User 28 | 29 | username = request.headers.get("X-Remote-User") 30 | if not username: 31 | return None 32 | 33 | # Create the user if it not yet exists 34 | user = User.query.filter_by(name=username).first() 35 | if not user: 36 | user = User(name=username) 37 | db.session.add(user) 38 | db.session.commit() 39 | 40 | return user 41 | 42 | 43 | @event.listens_for(Engine, "engine_connect") 44 | def ping_connection(connection, branch): 45 | # From http://docs.sqlalchemy.org/en/latest/core/pooling.html 46 | if branch or connection.should_close_with_result: 47 | # "branch" refers to a sub-connection of a connection, 48 | # "should_close_with_result" close request after result 49 | # we don't want to bother pinging on these. 50 | return 51 | 52 | try: 53 | # run a SELECT 1. use a core select() so that 54 | # the SELECT of a scalar value without a table is 55 | # appropriately formatted for the backend 56 | connection.scalar(select([1])) 57 | except exc.DBAPIError as err: 58 | # catch SQLAlchemy's DBAPIError, which is a wrapper 59 | # for the DBAPI's exception. It includes a .connection_invalidated 60 | # attribute which specifies if this connection is a "disconnect" 61 | # condition, which is based on inspection of the original exception 62 | # by the dialect in use. 63 | if err.connection_invalidated: 64 | # run the same SELECT again - the connection will re-validate 65 | # itself and establish a new connection. The disconnect detection 66 | # here also causes the whole connection pool to be invalidated 67 | # so that all stale connections are discarded. 68 | connection.scalar(select([1])) 69 | else: 70 | raise 71 | -------------------------------------------------------------------------------- /depc/extensions/flask_redis_cache.py: -------------------------------------------------------------------------------- 1 | from depc.utils.redis_cache import RedisCache 2 | 3 | 4 | class FlaskRedisCache(RedisCache): 5 | def __init__(self, app=None, config_prefix="REDIS_CACHE"): 6 | if app is not None: 7 | self.init_app(app, config_prefix) 8 | 9 | def init_app(self, app, config_prefix="REDIS_CACHE"): 10 | app_config = app.config.get(config_prefix, {}) 11 | RedisCache.__init__(self, **app_config) 12 | -------------------------------------------------------------------------------- /depc/logs/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | import sys 4 | 5 | from loguru import logger 6 | 7 | from depc.logs.handlers import InterceptHandler 8 | from depc.logs.sinks import GraylogExtendedLogFormatSink 9 | 10 | 11 | def setup_loggers(app): 12 | logging_config = app.config["LOGGING"] 13 | logging_level = logging.getLevelName(logging_config["level"]) 14 | 15 | # Avoid duplicate Flask logs 16 | werkzeug_logger = logging.getLogger("werkzeug") 17 | werkzeug_logger.handlers = [] 18 | 19 | root_logger = logging.getLogger() 20 | root_logger.addHandler(InterceptHandler()) 21 | root_logger.setLevel(logging_level) 22 | 23 | # Match some Flask messages 24 | # e.g.: '127.0.0.1 - - [01/Mar/2019 11:11:32] "GET /v1/teams/ 25 | # 1793e9bc-4724-477d-8d8e-a494b242d454/qos?start=1548979200&end=1551398399 26 | # HTTP/1.1" 200 -' 27 | regex = re.compile(r"\b((?:\d{1,3}\.){3}\d{1,3})\b - - \[.*\] \"(.*)\" (\d{1,3}) -") 28 | 29 | def stdout_filter(record): 30 | # Flask logging are handled by the Werkzeug module 31 | if record["name"] == "werkzeug._internal": 32 | flask_msg = regex.match(record["message"]) 33 | if flask_msg: 34 | # Rewrite message with the wanted values 35 | record["message"] = "{} {} {}".format( 36 | flask_msg.group(1), flask_msg.group(2), flask_msg.group(3) 37 | ) 38 | 39 | return True 40 | 41 | sink = sys.stdout 42 | is_serialized = False 43 | if logging_config.get("gelf"): 44 | sink = GraylogExtendedLogFormatSink 45 | is_serialized = True 46 | 47 | stdout_sink = { 48 | "sink": sink, 49 | "filter": stdout_filter, 50 | "level": logging_level, 51 | "serialize": is_serialized, 52 | } 53 | logging_format = logging_config.get("format") 54 | if logging_format: 55 | stdout_sink.update({"format": logging_format}) 56 | 57 | logger.configure(handlers=[stdout_sink]) 58 | -------------------------------------------------------------------------------- /depc/logs/handlers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from loguru import logger 4 | 5 | 6 | class InterceptHandler(logging.StreamHandler): 7 | def emit(self, record): 8 | logger_opt = logger.opt(depth=6, exception=record.exc_info) 9 | logger_opt.log(record.levelname, record.getMessage()) 10 | -------------------------------------------------------------------------------- /depc/logs/sinks.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class GraylogExtendedLogFormatSink: 5 | """ 6 | Log outputs into the Graylog Extended Log Format, a.k.a. GELF. 7 | """ 8 | 9 | def __init__(self, host="depc"): 10 | self.host = host 11 | 12 | def _transform_record_to_gelf(self, serialized_record): 13 | json_record = json.loads(serialized_record)["record"] 14 | 15 | # According to the documentation, extra fields are prefixed with "_". 16 | # See: https://docs.graylog.org/en/latest/pages/gelf.html#gelf-payload-specification 17 | payload = { 18 | "version": "1.1", 19 | "host": self.host, 20 | "short_message": json_record["message"], 21 | "level": json_record["level"]["name"], 22 | "timestamp": json_record["time"]["timestamp"], 23 | # "file" and "line" are sent as additional fields 24 | "_file": json_record["file"]["path"], 25 | "_line": json_record["line"], 26 | } 27 | 28 | # Add extra fields from logger.bind() to payload. 29 | for k, v in json_record["extra"].items(): 30 | payload["_" + k] = v 31 | 32 | return json.dumps(payload) 33 | 34 | def write(self, record): 35 | print(self._transform_record_to_gelf(record)) 36 | -------------------------------------------------------------------------------- /depc/models/checks.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.mutable import MutableDict 2 | from sqlalchemy_utils import UUIDType, JSONType 3 | 4 | from depc.extensions import db 5 | from depc.models import BaseModel 6 | 7 | 8 | class Check(BaseModel): 9 | 10 | __tablename__ = "checks" 11 | __repr_fields__ = ("name",) 12 | 13 | name = db.Column(db.String(255), nullable=False) 14 | 15 | source_id = db.Column( 16 | UUIDType(binary=False), db.ForeignKey("sources.id"), nullable=False 17 | ) 18 | source = db.relationship( 19 | "Source", backref=db.backref("source_checks", uselist=True) 20 | ) 21 | 22 | type = db.Column(db.String(255), nullable=False) 23 | parameters = db.Column(MutableDict.as_mutable(JSONType), default={}, nullable=True) 24 | 25 | variables = db.relationship("Variable", backref="check") 26 | -------------------------------------------------------------------------------- /depc/models/configs.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.mutable import MutableDict 2 | from sqlalchemy_utils import UUIDType, JSONType 3 | 4 | from depc.extensions import db 5 | from depc.models import BaseModel 6 | 7 | 8 | class Config(BaseModel): 9 | 10 | __tablename__ = "configs" 11 | __repr_fields__ = ("id", "team") 12 | 13 | team_id = db.Column( 14 | UUIDType(binary=False), db.ForeignKey("teams.id"), nullable=False 15 | ) 16 | team = db.relationship("Team", back_populates="configs") 17 | 18 | data = db.Column(MutableDict.as_mutable(JSONType), default={}, nullable=False) 19 | -------------------------------------------------------------------------------- /depc/models/news.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.schema import UniqueConstraint 2 | from sqlalchemy_utils import UUIDType 3 | 4 | from depc.extensions import db 5 | from depc.models import BaseModel 6 | 7 | 8 | users_news_association_table = db.Table( 9 | "users_news", 10 | db.Column( 11 | "user_id", UUIDType(binary=False), db.ForeignKey("users.id"), primary_key=True 12 | ), 13 | db.Column( 14 | "news_id", UUIDType(binary=False), db.ForeignKey("news.id"), primary_key=True 15 | ), 16 | UniqueConstraint("user_id", "news_id", name="users_news_uix"), 17 | ) 18 | 19 | 20 | class News(BaseModel): 21 | 22 | __tablename__ = "news" 23 | 24 | title = db.Column(db.String(100), nullable=False) 25 | message = db.Column(db.String(), nullable=False) 26 | users = db.relationship( 27 | "User", backref=db.backref("news", uselist=True), secondary="users_news" 28 | ) 29 | -------------------------------------------------------------------------------- /depc/models/rules.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.schema import UniqueConstraint 2 | from sqlalchemy_utils import UUIDType 3 | 4 | from depc.extensions import db 5 | from depc.models import BaseModel 6 | 7 | rule_check_association_table = db.Table( 8 | "rule_check_association", 9 | db.Column( 10 | "rule_id", UUIDType(binary=False), db.ForeignKey("rules.id"), primary_key=True 11 | ), 12 | db.Column( 13 | "check_id", UUIDType(binary=False), db.ForeignKey("checks.id"), primary_key=True 14 | ), 15 | UniqueConstraint("rule_id", "check_id", name="rule_check_uix"), 16 | ) 17 | 18 | 19 | class Rule(BaseModel): 20 | 21 | __tablename__ = "rules" 22 | __table_args__ = (UniqueConstraint("team_id", "name", name="team_rule_uc"),) 23 | __repr_fields__ = ("name",) 24 | 25 | name = db.Column(db.String(255), nullable=False) 26 | description = db.Column(db.String(), nullable=True) 27 | checks = db.relationship( 28 | "Check", 29 | backref=db.backref("rules", uselist=True), 30 | secondary="rule_check_association", 31 | ) 32 | 33 | team_id = db.Column( 34 | UUIDType(binary=False), db.ForeignKey("teams.id"), nullable=True 35 | ) 36 | team = db.relationship("Team", back_populates="rules") 37 | 38 | variables = db.relationship( 39 | "Variable", 40 | primaryjoin="and_(Rule.id==Variable.rule_id, " 41 | "Variable.source_id==None, " 42 | "Variable.check_id==None)", 43 | backref="rule", 44 | ) 45 | 46 | @property 47 | def recursive_variables(self): 48 | variables = { 49 | "rule": {v.name: v.value for v in self.variables}, 50 | "team": {}, 51 | "sources": {}, 52 | "checks": {}, 53 | } 54 | 55 | def _reformat(var): 56 | return {v.name: v.value for v in var} 57 | 58 | for check in self.checks: 59 | variables["checks"][check.name] = _reformat(check.variables) 60 | 61 | # The same source can appear, we add it once 62 | source = check.source 63 | if source.name not in variables["sources"]: 64 | variables["sources"][source.name] = _reformat(source.variables) 65 | 66 | # Every check is owned by the same team 67 | if not variables["team"]: 68 | variables["team"] = _reformat(source.team.variables) 69 | 70 | return variables 71 | -------------------------------------------------------------------------------- /depc/models/sources.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy_utils import UUIDType 2 | 3 | from depc.extensions import db 4 | from depc.extensions.encrypted_dict import EncryptedDict 5 | from depc.models import BaseModel 6 | 7 | 8 | class Source(BaseModel): 9 | 10 | __tablename__ = "sources" 11 | __repr_fields__ = ("name", "plugin") 12 | 13 | name = db.Column(db.String(255), nullable=False) 14 | 15 | plugin = db.Column(db.String(255), nullable=False) 16 | configuration = db.Column(EncryptedDict, default={}, nullable=True) 17 | 18 | checks = db.relationship("Check", back_populates="source") 19 | 20 | team_id = db.Column( 21 | UUIDType(binary=False), db.ForeignKey("teams.id"), nullable=True 22 | ) 23 | team = db.relationship("Team", back_populates="sources") 24 | 25 | variables = db.relationship( 26 | "Variable", 27 | primaryjoin="and_(Source.id==Variable.source_id, " 28 | "Variable.rule_id==None, " 29 | "Variable.check_id==None)", 30 | backref="source", 31 | ) 32 | -------------------------------------------------------------------------------- /depc/models/teams.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.associationproxy import association_proxy 2 | 3 | from depc.extensions import db 4 | from depc.extensions.encrypted_dict import EncryptedDict 5 | from depc.models import BaseModel 6 | 7 | 8 | class Team(BaseModel): 9 | 10 | __tablename__ = "teams" 11 | __repr_fields__ = ("name",) 12 | 13 | name = db.Column(db.String(255), nullable=False) 14 | sources = db.relationship("Source", back_populates="team") 15 | rules = db.relationship("Rule", back_populates="team") 16 | configs = db.relationship("Config", back_populates="team") 17 | worst = db.relationship("Worst", back_populates="team") 18 | 19 | grants = association_proxy("grants", "user") 20 | variables = db.relationship( 21 | "Variable", 22 | primaryjoin="and_(Team.id==Variable.team_id, " 23 | "Variable.rule_id==None, " 24 | "Variable.source_id==None, " 25 | "Variable.check_id==None)", 26 | backref="team", 27 | ) 28 | metas = db.Column(EncryptedDict, default={}, nullable=True) 29 | 30 | @property 31 | def members(self): 32 | return [grant.user for grant in self.grants if grant.role.value == "member"] 33 | 34 | @property 35 | def editors(self): 36 | return [grant.user for grant in self.grants if grant.role.value == "editor"] 37 | 38 | @property 39 | def managers(self): 40 | return [grant.user for grant in self.grants if grant.role.value == "manager"] 41 | 42 | @property 43 | def kafka_topic(self): 44 | return "".join(e for e in self.name if e.isalnum()).lower() 45 | -------------------------------------------------------------------------------- /depc/models/users.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | from flask_login import UserMixin 4 | from sqlalchemy.ext.associationproxy import association_proxy 5 | from sqlalchemy_utils import UUIDType 6 | 7 | from depc.extensions import db 8 | from depc.models import BaseModel 9 | 10 | 11 | class RoleNames(enum.Enum): 12 | member = "member" 13 | editor = "editor" 14 | manager = "manager" 15 | 16 | 17 | class Grant(BaseModel): 18 | __tablename__ = "grants" 19 | 20 | user_id = db.Column(UUIDType(binary=False), db.ForeignKey("users.id")) 21 | user = db.relationship("User", backref="grants") 22 | 23 | role = db.Column(db.Enum(RoleNames), nullable=False, unique=False) 24 | 25 | team_id = db.Column(UUIDType(binary=False), db.ForeignKey("teams.id")) 26 | team = db.relationship("Team", backref="grants") 27 | 28 | def __repr__(self): 29 | return "".format( 30 | self.user.name, self.role.value, self.team.name 31 | ) 32 | 33 | 34 | class User(BaseModel, UserMixin): 35 | 36 | __tablename__ = "users" 37 | __repr_fields__ = ("name",) 38 | 39 | name = db.Column(db.String(255), nullable=False, unique=True) 40 | admin = db.Column(db.Boolean(), default=False) 41 | active = db.Column(db.Boolean(), default=True) 42 | 43 | teams = association_proxy("grants", "team") 44 | 45 | def is_authenticated(self): 46 | return True 47 | 48 | def is_active(self): 49 | return self.active 50 | 51 | def is_anonymous(self): 52 | return False 53 | 54 | def is_admin(self): 55 | return self.admin 56 | 57 | def get_id(self): 58 | return self.id 59 | -------------------------------------------------------------------------------- /depc/models/variables.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy_utils import UUIDType 2 | 3 | from depc.extensions import db 4 | from depc.models import BaseModel 5 | 6 | 7 | class Variable(BaseModel): 8 | 9 | __tablename__ = "variables" 10 | __repr_fields__ = ("name", "value", "type") 11 | 12 | name = db.Column(db.String(255), nullable=False) 13 | value = db.Column(db.String(), nullable=False) 14 | type = db.Column(db.String(255), nullable=False) 15 | 16 | rule_id = db.Column( 17 | UUIDType(binary=False), db.ForeignKey("rules.id"), nullable=True 18 | ) 19 | team_id = db.Column( 20 | UUIDType(binary=False), db.ForeignKey("teams.id"), nullable=False 21 | ) 22 | source_id = db.Column( 23 | UUIDType(binary=False), db.ForeignKey("sources.id"), nullable=True 24 | ) 25 | check_id = db.Column( 26 | UUIDType(binary=False), db.ForeignKey("checks.id"), nullable=True 27 | ) 28 | 29 | @property 30 | def level(self): 31 | if self.check_id: 32 | return "check" 33 | elif self.source_id: 34 | return "source" 35 | elif self.rule_id: 36 | return "rule" 37 | else: 38 | return "team" 39 | 40 | @property 41 | def expression(self): 42 | exp = "depc.{level}['{name}']" if " " in self.name else "depc.{level}.{name}" 43 | return exp.format(level=self.level, name=self.name) 44 | -------------------------------------------------------------------------------- /depc/models/worst.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | from sqlalchemy.ext.mutable import MutableDict 4 | from sqlalchemy.schema import UniqueConstraint 5 | from sqlalchemy_utils import UUIDType, JSONType 6 | 7 | from depc.extensions import db 8 | from depc.models import BaseModel 9 | 10 | 11 | class Periods(enum.Enum): 12 | daily = "daily" 13 | monthly = "monthly" 14 | 15 | 16 | class Worst(BaseModel): 17 | 18 | __tablename__ = "worst" 19 | __table_args__ = ( 20 | UniqueConstraint( 21 | "team_id", "label", "date", "period", name="team_label_date_period_uc" 22 | ), 23 | ) 24 | __repr_fields__ = ("team", "label", "date", "period") 25 | 26 | team_id = db.Column( 27 | UUIDType(binary=False), db.ForeignKey("teams.id"), nullable=False 28 | ) 29 | team = db.relationship("Team", back_populates="worst") 30 | date = db.Column(db.DateTime(timezone=True), nullable=False) 31 | label = db.Column(db.String(255), nullable=False) 32 | period = db.Column(db.Enum(Periods), nullable=False, unique=False) 33 | data = db.Column(MutableDict.as_mutable(JSONType), default={}, nullable=False) 34 | -------------------------------------------------------------------------------- /depc/queries.py: -------------------------------------------------------------------------------- 1 | QOS_PER_TEAM = """ 2 | [ $token 'depc.qos.label' { 'team' '$TEAM$' } $start $end ] FETCH 3 | { '.app' '' } RELABEL 4 | INTERPOLATE 5 | """ 6 | 7 | QOS_FILTERED_BY_LABEL = """ 8 | [ $token 'depc.qos.label' { 'team' '$TEAM$' 'name' '$NAME$' } $start $end ] FETCH 9 | { '.app' '' } RELABEL 10 | INTERPOLATE 11 | """ 12 | 13 | # Specific item QOS 14 | QOS_ITEM_PER_TEAM_LABEL = """ 15 | { 16 | 'token' $token 17 | 'gts' 18 | [ 19 | NEWGTS 20 | 'depc.qos.node' RENAME 21 | { 22 | 'team' '$TEAM$' 23 | 'label' '$LABEL$' 24 | 'name' '$NAME$' 25 | } RELABEL 26 | ] 27 | 'start' $start 28 | 'end' $end 29 | } FETCH 30 | 31 | { '.app' '' } RELABEL 32 | """ 33 | 34 | # Get the statistics per team 35 | STATISTICS_SCRIPT = """ 36 | [ $token 37 | 'depc.qos.stats' 38 | { 'team' '$TEAM$' $FILTER$ } 39 | $start $end 40 | ] FETCH 41 | 42 | { '.app' '' } RELABEL 43 | """ 44 | 45 | # Worst items per label 46 | QOS_WORST_ITEM_PER_LABEL = """ 47 | $COUNT$ 'topN' STORE 48 | $topN 1 - 'topN' STORE 49 | 50 | [ $token 51 | 'depc.qos.node' 52 | { 'team' '$TEAM$' 'label' '$LABEL$' } 53 | $start $end 54 | ] FETCH 55 | 56 | // remove useless DATA 57 | { '.app' '' 'label' '' } RELABEL 58 | 59 | // compute mean value over the whole timespan 60 | [ SWAP bucketizer.mean 0 0 1 ] BUCKETIZE 61 | 62 | // sort the GTS (based on their latest value) 63 | LASTSORT 64 | 65 | // return results if any, else an empty stack [] 66 | DUP 67 | SIZE 'topSize' STORE 68 | 69 | <% $topSize 0 > %> 70 | <% [ 0 $topN ] SUBLIST %> 71 | IFT 72 | """ 73 | -------------------------------------------------------------------------------- /depc/schemas/v1_check.json: -------------------------------------------------------------------------------- 1 | { 2 | "check_input": { 3 | "type": "object", 4 | "additionalProperties": false, 5 | "required": ["name", "type", "parameters"], 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "Name of the check" 10 | }, 11 | "type": { 12 | "type": "string", 13 | "description": "Type of check" 14 | }, 15 | "parameters": { 16 | "type": "object" 17 | } 18 | } 19 | }, 20 | "check_update": { 21 | "type": "object", 22 | "additionalProperties": false, 23 | "properties": { 24 | "name": { 25 | "type": "string", 26 | "description": "Name of the check" 27 | }, 28 | "type": { 29 | "type": "string", 30 | "description": "Type of check" 31 | }, 32 | "parameters": { 33 | "type": "object" 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /depc/schemas/v1_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$comment": "ANY CHANGES APPLIED ON REGEX PATTERNS HERE SHOULD BE ADAPTED TO EXTRACTING PATTERNS AT: scheduler/dags/generate_dags.py", 3 | "config_input": { 4 | "type": "object", 5 | "patternProperties": { 6 | "^[A-Z]+[a-zA-Z0-9]*$": { 7 | "type": "object", 8 | "additionalProperties": false, 9 | "required": ["qos"], 10 | "properties": { 11 | "label": { 12 | "description": "Extra fields affecting the label", 13 | "type": "object", 14 | "additionalProperties": false, 15 | "required": ["average.exclude"], 16 | "properties": { 17 | "average.exclude": { 18 | "description": "Exclusion list of nodes from the compute of the Label average QOS", 19 | "type": "array", 20 | "items": { 21 | "type": "string" 22 | } 23 | } 24 | } 25 | }, 26 | "qos": { 27 | "description": "The QOS query", 28 | "type": "string", 29 | "anyOf": [{ 30 | "description": "Regex for QOS based on a rule", 31 | "type": "string", 32 | "pattern": "^rule.(.+|'.+')$" 33 | }, { 34 | "description": "Regex for QOS based on the AND & OR operations", 35 | "type": "string", 36 | "pattern": "^operation.(AND|OR)\\(?\\)?(\\[[A-Z]+[a-zA-Z0-9]*(, [A-Z]+[a-zA-Z0-9]*)*?\\])$" 37 | }, { 38 | "description": "Regex for QOS based on the ATLEAST operation", 39 | "type": "string", 40 | "pattern": "^operation.(ATLEAST)\\([0-9]+\\)(\\[[A-Z]+[a-zA-Z0-9]*(, [A-Z]+[a-zA-Z0-9]*)*?\\])$" 41 | }, { 42 | "description": "Regex for QOS based on the RATIO operation", 43 | "type": "string", 44 | "pattern": "^operation.(RATIO)\\(0.[0-9]+\\)(\\[[A-Z]+[a-zA-Z0-9]*(, [A-Z]+[a-zA-Z0-9]*)*?\\])$" 45 | }, { 46 | "description": "Regex for QOS based on an aggregation", 47 | "type": "string", 48 | "pattern": "^aggregation.(AVERAGE|MIN|MAX)\\(?\\)?(\\[[A-Z]+[a-zA-Z0-9]*(, [A-Z]+[a-zA-Z0-9]*)*?\\])$" 49 | }] 50 | } 51 | } 52 | } 53 | }, 54 | "additionalProperties": false 55 | } 56 | } -------------------------------------------------------------------------------- /depc/schemas/v1_label.json: -------------------------------------------------------------------------------- 1 | { 2 | "label_input": { 3 | "type": "object", 4 | "additionalProperties": false, 5 | "required": ["name"], 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "Name of the label" 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /depc/schemas/v1_rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "rule_input": { 3 | "type": "object", 4 | "additionalProperties": false, 5 | "required": [ 6 | "name" 7 | ], 8 | "properties": { 9 | "name": { 10 | "type": "string", 11 | "description": "Name of the rule" 12 | }, 13 | "description": { 14 | "type": "string", 15 | "description": "Description of the rule" 16 | } 17 | } 18 | }, 19 | "rule_execute": { 20 | "type": "object", 21 | "additionalProperties": false, 22 | "required": [ 23 | "name", 24 | "start", 25 | "end" 26 | ], 27 | "properties": { 28 | "name": { 29 | "type": "string", 30 | "description": "Name of the dependency" 31 | }, 32 | "end": { 33 | "type": "integer", 34 | "description": "The end timestamp" 35 | }, 36 | "start": { 37 | "type": "integer", 38 | "description": "The from timestamp" 39 | } 40 | } 41 | }, 42 | "rule_update": { 43 | "type": "object", 44 | "additionalProperties": false, 45 | "properties": { 46 | "name": { 47 | "type": "string", 48 | "description": "Name of the rule" 49 | }, 50 | "description": { 51 | "type": "string", 52 | "description": "Description of the rule" 53 | } 54 | } 55 | }, 56 | "rule_change_checks": { 57 | "type": "object", 58 | "additionalProperties": false, 59 | "required": ["checks"], 60 | "properties": { 61 | "checks": { 62 | "description": "Checks added in the rule", 63 | "type": "array", 64 | "items": { 65 | "type": "string" 66 | } 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /depc/schemas/v1_source.json: -------------------------------------------------------------------------------- 1 | { 2 | "source_input": { 3 | "type": "object", 4 | "additionalProperties": false, 5 | "required": ["name", "plugin", "configuration"], 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "Name of the source" 10 | }, 11 | "plugin": { 12 | "type": "string", 13 | "description": "Plugin to use for interaction with the source" 14 | }, 15 | "configuration": { 16 | "type": "object" 17 | } 18 | } 19 | }, 20 | "source_update": { 21 | "type": "object", 22 | "additionalProperties": false, 23 | "properties": { 24 | "name": { 25 | "type": "string", 26 | "description": "Name of the source" 27 | }, 28 | "plugin": { 29 | "type": "string", 30 | "description": "Plugin to use for interaction with the source" 31 | }, 32 | "configuration": { 33 | "type": "object" 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /depc/schemas/v1_team.json: -------------------------------------------------------------------------------- 1 | { 2 | "grants_input": { 3 | "type": "object", 4 | "additionalProperties": false, 5 | "required": ["grants"], 6 | "properties": { 7 | "grants": { 8 | "description": "Grants for the team", 9 | "type": "array", 10 | "items": { 11 | "type": "object", 12 | "required": ["user", "role"], 13 | "properties": { 14 | "user": { 15 | "description": "User to grant", 16 | "type": "string" 17 | }, 18 | "role": { 19 | "description": "Role to grant", 20 | "type": "string" 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /depc/schemas/v1_variable.json: -------------------------------------------------------------------------------- 1 | { 2 | "variable_input": { 3 | "type": "object", 4 | "additionalProperties": false, 5 | "required": ["name", "value", "type"], 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "Name of the variable" 10 | }, 11 | "value": { 12 | "type": "string", 13 | "description": "Value of the variable" 14 | }, 15 | "type": { 16 | "enum": ["string", "text", "number"] 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /depc/schemas/v1_warp.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-dependency": { 3 | "type": "object", 4 | "additionalProperties": false, 5 | "properties": { 6 | "domain": { 7 | "type": "string" 8 | }, 9 | "dependency": { 10 | "enum": ["Filer"] 11 | }, 12 | "value": { 13 | "type": "string" 14 | }, 15 | "date": { 16 | "type": "string", 17 | "format": "date-time" 18 | } 19 | }, 20 | "required": [ 21 | "domain", 22 | "dependency", 23 | "value", 24 | "date" 25 | ] 26 | } 27 | } -------------------------------------------------------------------------------- /depc/sources/exceptions.py: -------------------------------------------------------------------------------- 1 | class DataFetchException(Exception): 2 | pass 3 | 4 | 5 | class BadConfigurationException(Exception): 6 | pass 7 | 8 | 9 | class UnknownStateException(Exception): 10 | pass 11 | -------------------------------------------------------------------------------- /depc/sources/fake/__init__.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import logging 3 | 4 | import numpy as np 5 | 6 | from depc.sources import BaseSource, SourceRegister 7 | from depc.sources.exceptions import BadConfigurationException 8 | from depc.sources.fake.metrics import ( 9 | generate_fake_db_connections, 10 | generate_fake_http_status, 11 | generate_fake_oco_status, 12 | generate_fake_ping_data, 13 | ) 14 | 15 | logger = logging.getLogger(__name__) 16 | fake_plugin = SourceRegister() 17 | 18 | 19 | SCHEMA = {"type": "object", "properties": {}} 20 | FORM = [] 21 | 22 | 23 | @fake_plugin.source(schema=SCHEMA, form=FORM) 24 | class Fake(BaseSource): 25 | """ 26 | Fake source used in the documentation tutorial. 27 | """ 28 | 29 | name = "Fake" 30 | 31 | @classmethod 32 | def create_random_state(cls, start, end, name): 33 | """ 34 | Give a unique seed based on parameters 35 | """ 36 | slug = "{0}-{1}-{2}".format(start, end, name) 37 | seed = int(hashlib.sha1(slug.encode("utf-8")).hexdigest()[:7], 16) 38 | random_state = np.random.RandomState(seed) 39 | return random_state 40 | 41 | async def execute(self, parameters, name, start, end): 42 | metric = parameters["query"] 43 | 44 | # Our fake database just provides 4 metrics 45 | random_metrics_dispatcher = { 46 | "depc.tutorial.ping": generate_fake_ping_data, 47 | "depc.tutorial.oco": generate_fake_oco_status, 48 | "depc.tutorial.dbconnections": generate_fake_db_connections, 49 | "depc.tutorial.httpstatus": generate_fake_http_status, 50 | } 51 | 52 | if metric not in random_metrics_dispatcher.keys(): 53 | raise BadConfigurationException("Metric is not available for the tutorial") 54 | 55 | # Generate datapoints 56 | random_state = self.create_random_state(start, end, name) 57 | timestamps = list(map(int, np.arange(start, end, 60, dtype=int))) 58 | values = random_metrics_dispatcher[metric](random_state, len(timestamps)) 59 | dps = dict(zip(timestamps, values)) 60 | 61 | return [{"dps": dps, "metric": metric, "tags": {"name": name}}] 62 | -------------------------------------------------------------------------------- /depc/sources/opentsdb/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from json.decoder import JSONDecodeError 4 | 5 | import aiohttp 6 | 7 | from depc.sources import BaseSource, SourceRegister 8 | from depc.sources.exceptions import BadConfigurationException, DataFetchException 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | opentsdb_plugin = SourceRegister() 13 | 14 | 15 | SCHEMA = { 16 | "type": "object", 17 | "properties": { 18 | "url": { 19 | "title": "Url", 20 | "type": "string", 21 | "description": "The url used to query the database.", 22 | }, 23 | "credentials": { 24 | "title": "Credentials", 25 | "type": "string", 26 | "description": "The credentials used authenticate the queries.", 27 | }, 28 | }, 29 | "required": ["url", "credentials"], 30 | } 31 | 32 | 33 | FORM = [ 34 | {"key": "url", "placeholder": "http://127.0.0.1"}, 35 | {"key": "credentials", "placeholder": "foo:bar"}, 36 | ] 37 | 38 | 39 | @opentsdb_plugin.source(schema=SCHEMA, form=FORM) 40 | class OpenTSDB(BaseSource): 41 | """ 42 | Use an OpenTSDB database to launch your queries. 43 | """ 44 | 45 | name = "OpenTSDB" 46 | 47 | def build_query(self, name, start, end, parameters): 48 | try: 49 | query = { 50 | "start": int(start) - 1, 51 | "end": int(end), 52 | "queries": [json.loads(parameters["query"])], 53 | } 54 | except JSONDecodeError as e: 55 | raise BadConfigurationException( 56 | "Json Error in OpenTSDB query : {0}".format(str(e)) 57 | ) 58 | except ValueError: 59 | msg = "OpenTSDB Query is not valid : {}".format(parameters["query"]) 60 | raise BadConfigurationException(msg) 61 | 62 | return query 63 | 64 | async def execute(self, parameters, name, start, end): 65 | query = self.build_query(name, start, end, parameters) 66 | 67 | url = self.configuration["url"] + "/api/query" 68 | credentials = self.configuration["credentials"].split(":", maxsplit=1) 69 | 70 | auth = aiohttp.BasicAuth(credentials[0], credentials[1]) 71 | async with aiohttp.ClientSession(auth=auth) as session: 72 | async with session.post(url, json=query) as r: 73 | if r.status != 200: 74 | raise DataFetchException(str(r.text)) 75 | result = await r.json() 76 | 77 | # Convert the query result to be compliant with the Pandas compute 78 | timeseries = [] 79 | for ts in result: 80 | timeseries.append( 81 | { 82 | "metric": ts["metric"], 83 | "tags": ts["tags"], 84 | "dps": {int(k): v for k, v in ts["dps"].items()}, 85 | } 86 | ) 87 | 88 | return timeseries 89 | -------------------------------------------------------------------------------- /depc/sources/warp/__init__.py: -------------------------------------------------------------------------------- 1 | import html 2 | import logging 3 | import re 4 | 5 | from depc.sources import BaseSource, SourceRegister 6 | from depc.sources.exceptions import BadConfigurationException 7 | from depc.utils.warp10 import Warp10Client, Warp10Exception, _transform_warp10_values 8 | 9 | logger = logging.getLogger(__name__) 10 | warp_plugin = SourceRegister() 11 | 12 | 13 | SCHEMA = { 14 | "type": "object", 15 | "properties": { 16 | "url": { 17 | "title": "Url", 18 | "type": "string", 19 | "description": "The url used to launch the scripts.", 20 | }, 21 | "token": { 22 | "title": "Token", 23 | "type": "string", 24 | "description": "A read only token used authenticate the scripts.", 25 | }, 26 | }, 27 | "required": ["url", "token"], 28 | } 29 | 30 | 31 | FORM = [ 32 | {"key": "url", "placeholder": "http://127.0.0.1"}, 33 | {"key": "token", "placeholder": "foobar"}, 34 | ] 35 | 36 | 37 | @warp_plugin.source(schema=SCHEMA, form=FORM) 38 | class Warp10(BaseSource): 39 | """ 40 | Use a database Warp10 to launch your WarpScripts. 41 | """ 42 | 43 | name = "WarpScript" 44 | 45 | async def execute(self, parameters, name, start, end): 46 | client = Warp10Client( 47 | url=self.configuration["url"], rotoken=self.configuration["token"] 48 | ) 49 | 50 | # Generate the WarpScript and change the placeholders 51 | client.generate_script(start=start, end=end, script=parameters["query"]) 52 | 53 | try: 54 | response = await client.async_execute() 55 | except Warp10Exception as e: 56 | try: 57 | message = html.unescape( 58 | re.search("
(.*)<\/pre>", str(e)).groups()[0].strip()
59 |                 )
60 |             except Exception:
61 |                 message = str(e)
62 |             raise BadConfigurationException(
63 |                 "Warp10 Internal Error : {0}".format(message)
64 |             )
65 | 
66 |         # Transform the Warp10 values
67 |         timeseries = []
68 |         try:
69 |             for ts in response[0]:
70 |                 timeseries.append(
71 |                     {
72 |                         "dps": _transform_warp10_values(ts["v"]),
73 |                         "metric": ts["c"],
74 |                         "tags": ts["l"],
75 |                     }
76 |                 )
77 | 
78 |         # Response is not parsable, return it to the user for debugging
79 |         except TypeError:
80 |             raise BadConfigurationException(
81 |                 "Script does not return valid format : {}".format(response)
82 |             )
83 | 
84 |         return timeseries
85 | 


--------------------------------------------------------------------------------
/depc/templates/admin/cache.html:
--------------------------------------------------------------------------------
 1 | {% extends "admin/master.html" %}
 2 | 
 3 | {% block head_css %}
 4 | {{ super() }}
 5 | 
 6 | {% endblock %}
 7 | 
 8 | {% block body %}
 9 | 
10 | 
11 |
12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 | 23 |
24 | 25 | {% if number_of_keys %} 26 | Confirm deletion of all keys in database ({{ number_of_keys }} keys)? 27 |
28 | 29 | 30 |
31 | {% endif %} 32 | 33 | {% if keys %} 34 | 35 | Confirm deletion of the selected keys ({{ keys | length }} keys)? 36 |
37 | 38 | 39 | 40 |
41 | 42 |

Key list:

43 | {% for key in keys %}
  • {{ key }}
  • {% endfor %} 44 | 45 | {% endif %} 46 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /depc/users.py: -------------------------------------------------------------------------------- 1 | from flask_login import current_user 2 | 3 | from depc.controllers.teams import TeamController 4 | 5 | 6 | class TeamPermission: 7 | @classmethod 8 | def _get_team(cls, team_id): 9 | obj = TeamController._get(filters={"Team": {"id": team_id}}) 10 | 11 | # Add the members of the team 12 | team = TeamController.resource_to_dict(obj) 13 | team.update( 14 | { 15 | "members": [m.name for m in obj.members], 16 | "editors": [m.name for m in obj.editors], 17 | "managers": [m.name for m in obj.managers], 18 | } 19 | ) 20 | return team 21 | 22 | @classmethod 23 | def is_user(cls, team_id): 24 | team = cls._get_team(team_id) 25 | team_users = team["members"] + team["editors"] + team["managers"] 26 | return current_user.name in team_users 27 | 28 | @classmethod 29 | def is_manager_or_editor(cls, team_id): 30 | team = cls._get_team(team_id) 31 | team_users = team["editors"] + team["managers"] 32 | return current_user.name in team_users 33 | 34 | @classmethod 35 | def is_manager(cls, team_id): 36 | return current_user.name in cls._get_team(team_id)["managers"] 37 | 38 | @classmethod 39 | def is_editor(cls, team_id): 40 | return current_user.name in cls._get_team(team_id)["editors"] 41 | 42 | @classmethod 43 | def is_member(cls, team_id): 44 | return current_user.name in cls._get_team(team_id)["members"] 45 | -------------------------------------------------------------------------------- /depc/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import arrow 4 | 5 | 6 | def is_uuid(data): 7 | """Check is data is a valid uuid. If data is a list, 8 | checks if all elements of the list are valid uuids""" 9 | temp = [data] if not isinstance(data, list) else data 10 | for i in temp: 11 | try: 12 | uuid.UUID(str(i), version=4) 13 | except ValueError: 14 | return False 15 | return True 16 | 17 | 18 | def to_list(obj): 19 | """ Return a list containing obj if obj is not already an iterable""" 20 | try: 21 | iter(obj) 22 | return obj 23 | except TypeError: 24 | return [obj] 25 | 26 | 27 | def get_start_end_ts(day=None): 28 | if not day: 29 | # yesterday at midnight 30 | date = arrow.utcnow().shift(days=-1).floor("day") 31 | else: 32 | # given day, at midnight (arrow works in UTC by default) 33 | date = arrow.get(day) 34 | 35 | start = date.timestamp 36 | end = date.ceil("day").timestamp 37 | 38 | return start, end 39 | -------------------------------------------------------------------------------- /depc_dependencies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/depc_dependencies.png -------------------------------------------------------------------------------- /depc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/depc_logo.png -------------------------------------------------------------------------------- /depc_qos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/depc_qos.png -------------------------------------------------------------------------------- /depc_screenshots.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/depc_screenshots.gif -------------------------------------------------------------------------------- /depc_screenshots_small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/depc_screenshots_small.gif -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | CONFIG_FILE='depc.prod.yml' 5 | 6 | if [[ $1 ]]; then 7 | APP_TYPE=$1 8 | fi 9 | 10 | if [[ "$APP_TYPE" = "api" || -z $APP_TYPE ]];then 11 | 12 | # Launch Gunicorn 13 | export G_WORKERS=${G_WORKERS:=5} 14 | export G_THREADS=${G_THREADS:=1} 15 | export G_MAX_REQUESTS=${G_MAX_REQUESTS:=1000} 16 | export G_MAX_REQUESTS_JITTER=${G_MAX_REQUESTS_JITTER:=20} 17 | export G_BACKLOG=${G_BACKLOG:=5} 18 | export G_TIMEOUT=${G_TIMEOUT:=300} 19 | export G_GRACEFUL_TIMEOUT=${G_GRACEFUL_TIMEOUT:=300} 20 | 21 | echo "Starting API" 22 | exec gunicorn --bind 0.0.0.0:5000 --workers $G_WORKERS \ 23 | --threads $G_THREADS --backlog $G_BACKLOG --timeout $G_TIMEOUT \ 24 | --graceful-timeout $G_GRACEFUL_TIMEOUT --access-logfile - \ 25 | --access-logformat '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s %(L)ss "%(f)s" "%(a)s"' \ 26 | --max-requests $G_MAX_REQUESTS --max-requests-jitter $G_MAX_REQUESTS_JITTER \ 27 | manage:app 28 | 29 | elif [ "$APP_TYPE" = "consumer" ];then 30 | 31 | echo "Starting the Kafka consumer" 32 | make consumer 33 | 34 | else 35 | 36 | echo "Wrong argument : $APP_TYPE" 37 | 38 | fi 39 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .wy-side-nav-search { 2 | background-color: #2c475b; 3 | } -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | .wy-side-nav-search { 2 | background-color: red !important; 3 | } -------------------------------------------------------------------------------- /docs/_static/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/architecture.png -------------------------------------------------------------------------------- /docs/_static/images/depc-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/depc-logo.png -------------------------------------------------------------------------------- /docs/_static/images/depc_screenshots.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/depc_screenshots.gif -------------------------------------------------------------------------------- /docs/_static/images/guides/grafana/details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/guides/grafana/details.png -------------------------------------------------------------------------------- /docs/_static/images/guides/grafana/export_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/guides/grafana/export_button.png -------------------------------------------------------------------------------- /docs/_static/images/guides/grafana/export_modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/guides/grafana/export_modal.png -------------------------------------------------------------------------------- /docs/_static/images/guides/grafana/import_json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/guides/grafana/import_json.png -------------------------------------------------------------------------------- /docs/_static/images/guides/grafana/import_json_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/guides/grafana/import_json_2.png -------------------------------------------------------------------------------- /docs/_static/images/guides/grafana/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/guides/grafana/summary.png -------------------------------------------------------------------------------- /docs/_static/images/guides/indicators/interval.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/guides/indicators/interval.png -------------------------------------------------------------------------------- /docs/_static/images/guides/indicators/simple_threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/guides/indicators/simple_threshold.png -------------------------------------------------------------------------------- /docs/_static/images/guides/variables/builtins_variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/guides/variables/builtins_variables.png -------------------------------------------------------------------------------- /docs/_static/images/guides/variables/indicator_variable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/guides/variables/indicator_variable.png -------------------------------------------------------------------------------- /docs/_static/images/guides/variables/jinja_condition_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/guides/variables/jinja_condition_1.png -------------------------------------------------------------------------------- /docs/_static/images/guides/variables/jinja_condition_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/guides/variables/jinja_condition_2.png -------------------------------------------------------------------------------- /docs/_static/images/guides/variables/jinja_condition_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/guides/variables/jinja_condition_3.png -------------------------------------------------------------------------------- /docs/_static/images/installation/airflow_webserver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/installation/airflow_webserver.png -------------------------------------------------------------------------------- /docs/_static/images/installation/airflow_webserver_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/installation/airflow_webserver_config.png -------------------------------------------------------------------------------- /docs/_static/images/installation/create_team.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/installation/create_team.png -------------------------------------------------------------------------------- /docs/_static/images/installation/empty_homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/installation/empty_homepage.png -------------------------------------------------------------------------------- /docs/_static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/logo.png -------------------------------------------------------------------------------- /docs/_static/images/neo4j-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/neo4j-logo.png -------------------------------------------------------------------------------- /docs/_static/images/qos_dependencies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/qos_dependencies.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/acme_team.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/acme_team.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/all_fake_checks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/all_fake_checks.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/associate_filer_servers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/associate_filer_servers.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/attach_server_ping_check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/attach_server_ping_check.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/attach_servers_checks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/attach_servers_checks.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/check_server_ping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/check_server_ping.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/dashboard1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/dashboard1.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/dashboard2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/dashboard2.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/dashboard3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/dashboard3.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/dependencies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/dependencies.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/dependencies_association.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/dependencies_association.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/dependencies_qos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/dependencies_qos.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/dependencies_website_filer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/dependencies_website_filer.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/empty_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/empty_dashboard.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/graph_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/graph_api.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/kafka1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/kafka1.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/kafka2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/kafka2.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/kafka3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/kafka3.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/new_fake_source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/new_fake_source.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/new_filers_rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/new_filers_rule.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/qos_evolution_all_labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/qos_evolution_all_labels.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/qos_evolution_specific_label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/qos_evolution_specific_label.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/qos_evolution_specific_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/qos_evolution_specific_node.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/qos_evolution_specific_node_details1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/qos_evolution_specific_node_details1.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/qos_evolution_specific_node_details2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/qos_evolution_specific_node_details2.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/rule_details_http_status_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/rule_details_http_status_code.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/rule_details_max_dbconnections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/rule_details_max_dbconnections.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/rule_details_server_oco.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/rule_details_server_oco.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/rule_details_server_ping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/rule_details_server_ping.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/rule_filers_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/rule_filers_result.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/rule_launched_summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/rule_launched_summary.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/rule_range_yesterday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/rule_range_yesterday.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/team_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/team_id.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/update_configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/update_configuration.png -------------------------------------------------------------------------------- /docs/_static/images/tutorial/website_dependencies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/docs/_static/images/tutorial/website_dependencies.png -------------------------------------------------------------------------------- /docs/api/checks.rst: -------------------------------------------------------------------------------- 1 | Checks 2 | ====== 3 | 4 | .. qrefflask:: manage:app 5 | :undoc-static: 6 | :include-empty-docstring: 7 | :modules: depc.apiv1.checks 8 | 9 | .. autoflask:: manage:app 10 | :undoc-static: 11 | :include-empty-docstring: 12 | :modules: depc.apiv1.checks 13 | -------------------------------------------------------------------------------- /docs/api/configs.rst: -------------------------------------------------------------------------------- 1 | Configs 2 | ======= 3 | 4 | .. qrefflask:: manage:app 5 | :undoc-static: 6 | :include-empty-docstring: 7 | :modules: depc.apiv1.configs 8 | 9 | .. autoflask:: manage:app 10 | :undoc-static: 11 | :include-empty-docstring: 12 | :modules: depc.apiv1.configs 13 | -------------------------------------------------------------------------------- /docs/api/dependencies.rst: -------------------------------------------------------------------------------- 1 | Dependencies 2 | ============ 3 | 4 | .. qrefflask:: manage:app 5 | :undoc-static: 6 | :include-empty-docstring: 7 | :modules: depc.apiv1.dependencies 8 | 9 | .. autoflask:: manage:app 10 | :undoc-static: 11 | :include-empty-docstring: 12 | :modules: depc.apiv1.dependencies 13 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. toctree:: 5 | :titlesonly: 6 | :maxdepth: 2 7 | 8 | checks 9 | configs 10 | dependencies 11 | news 12 | qos 13 | rules 14 | sources 15 | statistics 16 | teams 17 | users 18 | variables 19 | -------------------------------------------------------------------------------- /docs/api/news.rst: -------------------------------------------------------------------------------- 1 | News 2 | ==== 3 | 4 | .. qrefflask:: manage:app 5 | :undoc-static: 6 | :include-empty-docstring: 7 | :modules: depc.apiv1.news 8 | 9 | .. autoflask:: manage:app 10 | :undoc-static: 11 | :include-empty-docstring: 12 | :modules: depc.apiv1.news 13 | -------------------------------------------------------------------------------- /docs/api/qos.rst: -------------------------------------------------------------------------------- 1 | Qos 2 | === 3 | 4 | .. qrefflask:: manage:app 5 | :undoc-static: 6 | :include-empty-docstring: 7 | :modules: depc.apiv1.qos 8 | 9 | .. autoflask:: manage:app 10 | :undoc-static: 11 | :include-empty-docstring: 12 | :modules: depc.apiv1.qos 13 | -------------------------------------------------------------------------------- /docs/api/rules.rst: -------------------------------------------------------------------------------- 1 | Rules 2 | ===== 3 | 4 | .. qrefflask:: manage:app 5 | :undoc-static: 6 | :include-empty-docstring: 7 | :modules: depc.apiv1.rules 8 | 9 | .. autoflask:: manage:app 10 | :undoc-static: 11 | :include-empty-docstring: 12 | :modules: depc.apiv1.rules 13 | -------------------------------------------------------------------------------- /docs/api/sources.rst: -------------------------------------------------------------------------------- 1 | Sources 2 | ======= 3 | 4 | .. qrefflask:: manage:app 5 | :undoc-static: 6 | :include-empty-docstring: 7 | :modules: depc.apiv1.sources 8 | 9 | .. autoflask:: manage:app 10 | :undoc-static: 11 | :include-empty-docstring: 12 | :modules: depc.apiv1.sources 13 | -------------------------------------------------------------------------------- /docs/api/statistics.rst: -------------------------------------------------------------------------------- 1 | Statistics 2 | ========== 3 | 4 | .. qrefflask:: manage:app 5 | :undoc-static: 6 | :include-empty-docstring: 7 | :modules: depc.apiv1.statistics 8 | 9 | .. autoflask:: manage:app 10 | :undoc-static: 11 | :include-empty-docstring: 12 | :modules: depc.apiv1.statistics 13 | -------------------------------------------------------------------------------- /docs/api/teams.rst: -------------------------------------------------------------------------------- 1 | Teams 2 | ===== 3 | 4 | .. qrefflask:: manage:app 5 | :undoc-static: 6 | :include-empty-docstring: 7 | :modules: depc.apiv1.teams 8 | 9 | .. autoflask:: manage:app 10 | :undoc-static: 11 | :include-empty-docstring: 12 | :modules: depc.apiv1.teams 13 | -------------------------------------------------------------------------------- /docs/api/users.rst: -------------------------------------------------------------------------------- 1 | Users 2 | ===== 3 | 4 | .. qrefflask:: manage:app 5 | :undoc-static: 6 | :include-empty-docstring: 7 | :modules: depc.apiv1.users 8 | 9 | .. autoflask:: manage:app 10 | :undoc-static: 11 | :include-empty-docstring: 12 | :modules: depc.apiv1.users 13 | -------------------------------------------------------------------------------- /docs/api/variables.rst: -------------------------------------------------------------------------------- 1 | Variables 2 | ========= 3 | 4 | .. qrefflask:: manage:app 5 | :undoc-static: 6 | :include-empty-docstring: 7 | :modules: depc.apiv1.variables 8 | 9 | .. autoflask:: manage:app 10 | :undoc-static: 11 | :include-empty-docstring: 12 | :modules: depc.apiv1.variables 13 | -------------------------------------------------------------------------------- /docs/guides/difference-qos-sla.rst: -------------------------------------------------------------------------------- 1 | .. _difference-qos-sla: 2 | 3 | QoS and SLA 4 | =========== 5 | 6 | One of the feature provided by DepC is to compute the QoS of your nodes, 7 | whether servers, web services or even customers. So it's important to know 8 | the difference between QoS and SLA, but also what are SLO and SLI. 9 | 10 | QoS (Quality of Service) 11 | ------------------------ 12 | 13 | A QoS is a percentage telling you what is (or what was) the state of a specific 14 | service. This state is very subjective but in general it refers to the ability 15 | for a customer to consume its services in good conditions. So we can talk here 16 | about the **availability** of the service. 17 | 18 | For example in DepC, a website which is available **from 0:00:00 to 23:59:59** 19 | will have a QoS of **100%**. This QoS can be decreased each time the website is 20 | not reachable. In the extreme case where he would not be reachable for an entire 21 | day, it's QoS will be **0%**. 22 | 23 | SLA (Service Level Agreement) 24 | ----------------------------- 25 | 26 | A SLA is a contract between a provider and its customers, telling it what is 27 | the awaited quality of service. Being a commitment signed by both parties, the 28 | SLA can also contains penalties if the quality is not good. 29 | 30 | So it's possible to see mentions about QoS in a SLA, for example "we commit 31 | ourselves to provide an uptime of 99.99% for the product XXX". But keep in mind 32 | that a **QoS is not the same as a SLA**. 33 | 34 | SLO (Service Level Objectif) 35 | ---------------------------- 36 | 37 | The SLO refers to the objective that a provider wants to reach in term of QoS. 38 | Let's imagine the QoS of a specific service is 99.95% : the provider may 39 | want to improve this percentage and give a SLO of 99.99%. 40 | 41 | SLI (Service Level Indicator) 42 | ----------------------------- 43 | 44 | The SLI is in the heart of DepC : an indicator is a measurement used to compute 45 | a QoS (and so to reach a SLO). It can be everything that can be measurable over 46 | a period of time : 47 | 48 | - an HTTP Status Code, 49 | - the response time of a Ping, 50 | - the RAM usage, 51 | - ... and so on. 52 | 53 | DepC uses TimeSeries databases to retrieve raw data. The idea is pretty simple : 54 | let's imagine we need to compute the QoS of an API : we could analyse the HTTP 55 | status code and reject every status which is above 500 (internal server error). 56 | 57 | If we analyse a period containing 100 HTTP status code (called datapoints in a 58 | Time Series DB) with 20 status above 500, the QoS will be **80%** (100 - 20). 59 | 60 | DepC is able to combine multiple :ref:`indicators ` to compute a 61 | QoS. 62 | -------------------------------------------------------------------------------- /docs/guides/grafana.rst: -------------------------------------------------------------------------------- 1 | .. _grafana: 2 | 3 | Grafana 4 | ======= 5 | 6 | DepC provides two templates to bootstrap Grafana dashboards with information 7 | concerning your QoS. 8 | 9 | Installation 10 | ------------ 11 | 12 | .. warning:: 13 | You have to be manager of your team to use this feature. 14 | 15 | You must create the DepC source in your Grafana instance using the ``direct`` 16 | access. 17 | 18 | You must also create the ``token`` constant containing your read-only token. 19 | You can display it when you open a view throught the **Export to Grafana** 20 | button available in your DepC homepage : 21 | 22 | .. figure:: ../_static/images/guides/grafana/export_button.png 23 | :alt: Export Button 24 | :align: center 25 | 26 | .. figure:: ../_static/images/guides/grafana/export_modal.png 27 | :alt: Export Modal 28 | :align: center 29 | 30 | You can now copy the JSON of your wanted view (repeat the following 31 | operation for each view) and paste it in Grafana using the **Import** 32 | menu : 33 | 34 | .. figure:: ../_static/images/guides/grafana/import_json.png 35 | :alt: Import JSON 36 | :align: center 37 | 38 | .. figure:: ../_static/images/guides/grafana/import_json_2.png 39 | :alt: Import JSON 40 | :align: center 41 | 42 | .. note:: 43 | We only provide Warp10 export for now. 44 | 45 | Summary view 46 | ------------ 47 | 48 | This dashboard displays your average QoS and the evolution of each 49 | label. You can also quickly view the worst days by label. 50 | 51 | .. figure:: ../_static/images/guides/grafana/summary.png 52 | :alt: QoS Summary 53 | 54 | 55 | Details view 56 | ------------ 57 | 58 | You can use this dashboard to display the evolution of a specific node, 59 | filtering it by label and by name. 60 | 61 | .. figure:: ../_static/images/guides/grafana/details.png 62 | :alt: QoS Details 63 | 64 | -------------------------------------------------------------------------------- /docs/guides/index.rst: -------------------------------------------------------------------------------- 1 | .. _guides: 2 | 3 | Guides 4 | ====== 5 | 6 | You'll find in this section some guides to better understand 7 | some of DepC features : 8 | 9 | .. toctree:: 10 | :titlesonly: 11 | :maxdepth: 2 12 | 13 | indicators 14 | kafka 15 | difference-qos-sla 16 | queries 17 | sources 18 | variables 19 | grafana 20 | -------------------------------------------------------------------------------- /docs/guides/indicators.rst: -------------------------------------------------------------------------------- 1 | .. _indicators: 2 | 3 | Indicators 4 | ========== 5 | 6 | Overview 7 | -------- 8 | 9 | .. warning:: 10 | 11 | Checks are renamed **Indicators**. 12 | 13 | An indicator is a Python function that queries a source, retrieves the data 14 | and uses it to compute a QOS percentage. An indicator is included in a rule : 15 | when we execute a rule, we execute its indicator(s). 16 | 17 | Concretely an indicator converts some datapoints into a percentage : 18 | 19 | .. code:: json 20 | 21 | { 22 | "1513855920": 109, 23 | "1513856040": 113, 24 | "1513856160": 125, 25 | [...] 26 | "1513890000": 114 27 | } 28 | 29 | After having been processed by the indicator, these datapoints will be 30 | converted into a QOS (e.g. **99.456%**). 31 | 32 | Simple Threshold 33 | ---------------- 34 | 35 | This indicator defines a threshold when the datapoints will be considered as valid. 36 | Every datapoints which are above a given threshold will lower the QOS. 37 | 38 | Threshold examples : ``200``, ``200.1``, ``-200``, ``-200.1``, ... 39 | 40 | In this example, the threshold is defined to ``200`` and the effect is illustrated below : 41 | 42 | .. figure:: ../_static/images/guides/indicators/simple_threshold.png 43 | :alt: Simple Threshold Indicator 44 | 45 | Interval 46 | -------- 47 | 48 | Likewise the threshold indicator, but datapoints which are not into a given 49 | interval lower the QOS. 50 | 51 | Interval examples : ``200:300``, ``-200:200``, ``-300:-200``, ``200.1:300``, ... 52 | 53 | In this example, the threshold is defined to ``200:300`` and the effect is illustrated below : 54 | 55 | .. figure:: ../_static/images/guides/indicators/interval.png 56 | :alt: Interval Indicator 57 | 58 | .. note:: 59 | Note these examples just use a few datapoints, but 60 | of course it can be much more in reality (like dozens of thousands). 61 | This will improve the accuracy of your QOS. 62 | -------------------------------------------------------------------------------- /docs/guides/sources.rst: -------------------------------------------------------------------------------- 1 | .. sources: 2 | 3 | Sources Configuration 4 | ===================== 5 | 6 | DepC currently supports 2 TimeSeries databases : **OpenTSDB** and **Warp10**. 7 | 8 | OpenTSDB 9 | -------- 10 | 11 | OpenTSDB is a scalable and distributed time series database. Please refer to 12 | the `official documentation `__ 13 | to learn how to query OpenTSDB. 14 | 15 | DepC needs 2 information to query an OpenTSDB database : 16 | 17 | - a URL : ``http://my-opentsdb.local`` 18 | - the credentials separated by a colon : ``myuser:mypassword`` 19 | 20 | Here is a really simple payload, to show you how to use it in your indicators : 21 | 22 | .. code:: json 23 | 24 | { 25 | "tags": { 26 | "name": "{{ depc.name }}" 27 | }, 28 | "aggregator": "avg", 29 | "metric": "my.awesome.metric" 30 | } 31 | 32 | 33 | Warp10 34 | ------ 35 | 36 | Warp10 is a Geo Time Series database. DepC uses the ``/exec`` endpoint to 37 | query it, so please refer to the `official documentation 38 | `__ 39 | to learn how to query Warp10. 40 | 41 | DepC needs 2 information to query a Warp10 database : 42 | 43 | - a URL containing the API version : ``http://my-warp10.local/api/v0`` 44 | - a read-only token : ``myrotoken`` 45 | 46 | Here is a really simple payload, to show you how to use it in your indicators : 47 | 48 | .. code:: 49 | 50 | [ $token 51 | 'my.awesome.metric' 52 | { 'name' '{{ depc.name }}' } 53 | $start $end 54 | ] FETCH SORT 55 | 56 | As you can see you can access 3 variables populated by DepC when running 57 | your indicator : 58 | 59 | - ``$token`` : the token provided in the source configuration, 60 | - ``$start`` : the start time of the analysed period, in ISO8601 format, 61 | - ``$end`` : the end time of the analysed period, in ISO8601 format. 62 | -------------------------------------------------------------------------------- /docs/guides/variables.rst: -------------------------------------------------------------------------------- 1 | Variables 2 | ========= 3 | 4 | DepC allows you to declare a variable that can be reused in 5 | your **indicator parameters**. 6 | 7 | A variable can be of the following types : 8 | 9 | - a string 10 | - a text 11 | - a number 12 | 13 | You can create it in the **Variables** menu within your team, for 14 | example : 15 | 16 | .. figure:: ../_static/images/guides/variables/indicator_variable.png 17 | :alt: Indicator Variable 18 | 19 | .. warning:: 20 | 21 | Checks are renamed **Indicators**. But the variable namespace prefix 22 | remains ``depc.check.`` for now. 23 | 24 | Here we declare a variable in the **Indicator** level : it means 25 | ``{{ depc.check.location }}`` will be replaced by the *FR2* value 26 | in all uses. 27 | 28 | Levels 29 | ------ 30 | 31 | To be as flexible as possible, DepC supports 4 levels of variables : 32 | 33 | - Team : ``{{ depc.team.myTeamVar }}`` 34 | - Rule : ``{{ depc.rule.myRuleVar }}`` 35 | - Source : ``{{ depc.source.mySourceVar }}`` 36 | - Indicator (previously Check) : ``{{ depc.check.myIndicatorVar }}`` 37 | 38 | Builtins Variables 39 | ------------------ 40 | 41 | By default 3 builtins variables are available : 42 | 43 | - Name : ``{{ depc.name }}`` 44 | - Start: ``{{ depc.start }}`` 45 | - End: ``{{ depc.end }}`` 46 | 47 | These variables represent the 3 arguments sent to every indicators when 48 | launching a rule : 49 | 50 | .. figure:: ../_static/images/guides/variables/builtins_variables.png 51 | :alt: Builtins Variables 52 | 53 | Jinja2 Templating 54 | ----------------- 55 | 56 | After declaring it, you can reuse your variables with the 57 | `Jinja2 `__ syntax (calling the variables 58 | can be done within the ``{{ ... }}`` delimiters). 59 | 60 | .. note:: 61 | You can use the **iso8601** filter after your 62 | timestamp variables : it will convert the timestamp into its iso8601 63 | value (``{{ depc.start | iso8601 }}``). 64 | 65 | You can also use the Jinja2 conditions. For example your can filter your 66 | timeseries by tags if the name was sent during the rule execution : 67 | 68 | .. figure:: ../_static/images/guides/variables/jinja_condition_1.png 69 | :alt: Jinja Condition 1 70 | 71 | .. figure:: ../_static/images/guides/variables/jinja_condition_2.png 72 | :alt: Jinja Condition 2 73 | 74 | .. figure:: ../_static/images/guides/variables/jinja_condition_3.png 75 | :alt: Jinja Condition 3 76 | 77 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from depc import create_app 4 | from depc.commands.config import config_cli 5 | from depc.commands.key import key_cli 6 | from depc.commands.user import user_cli 7 | 8 | env = os.getenv("DEPC_ENV", "dev") 9 | 10 | app = create_app(environment=env) 11 | app.cli.add_command(config_cli) 12 | app.cli.add_command(key_cli) 13 | app.cli.add_command(user_cli) 14 | 15 | 16 | if __name__ == "__main__": 17 | app.run(debug=(env == "dev")) 18 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/48570234e11c_add_the_news_table.py: -------------------------------------------------------------------------------- 1 | """Add the news table 2 | 3 | Revision ID: 48570234e11c 4 | Revises: 956e9a3dd287 5 | Create Date: 2019-06-17 15:16:29.844841 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy_utils import UUIDType 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '48570234e11c' 15 | down_revision = '956e9a3dd287' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.create_table('news', 23 | sa.Column('id', UUIDType(binary=False), nullable=False), 24 | sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), 25 | sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), 26 | sa.Column('title', sa.String(length=100), nullable=False), 27 | sa.Column('message', sa.String(), nullable=False), 28 | sa.PrimaryKeyConstraint('id') 29 | ) 30 | op.create_index(op.f('ix_news_created_at'), 'news', ['created_at'], unique=False) 31 | op.create_table('users_news', 32 | sa.Column('user_id', UUIDType(binary=False), nullable=False), 33 | sa.Column('news_id', UUIDType(binary=False), nullable=False), 34 | sa.ForeignKeyConstraint(['news_id'], ['news.id'], ), 35 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 36 | sa.PrimaryKeyConstraint('user_id', 'news_id'), 37 | sa.UniqueConstraint('user_id', 'news_id', name='users_news_uix') 38 | ) 39 | # ### end Alembic commands ### 40 | 41 | 42 | def downgrade(): 43 | # ### commands auto generated by Alembic - please adjust! ### 44 | op.drop_constraint('users_news_uix', 'users_news', type_='unique') 45 | op.drop_table('users_news') 46 | op.drop_index(op.f('ix_news_created_at'), table_name='news') 47 | op.drop_table('news') 48 | # ### end Alembic commands ### 49 | -------------------------------------------------------------------------------- /migrations/versions/88c2ab34728c_rename_check_parameters.py: -------------------------------------------------------------------------------- 1 | """Rename check parameters 2 | 3 | Revision ID: 88c2ab34728c 4 | Revises: d2dc7ff020a0 5 | Create Date: 2019-06-28 11:42:47.081483 6 | 7 | """ 8 | from sqlalchemy.exc import ProgrammingError 9 | 10 | from depc.extensions import db 11 | from depc.models.checks import Check 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision = "88c2ab34728c" 16 | down_revision = "d2dc7ff020a0" 17 | branch_labels = None 18 | depends_on = None 19 | 20 | 21 | KEYS_MAPPING = {"Fake": "metric", "OpenTSDB": "query", "WarpScript": "script"} 22 | 23 | 24 | def upgrade(): 25 | """ 26 | This code upgrades the format of the checks data, not the schema 27 | itself. We need to verify if we're in a fresh DepC installation or 28 | if it's a DepC version upgrade. 29 | """ 30 | try: 31 | checks = Check.query.all() 32 | 33 | # Handles the 'relation "checks" does not exist' error 34 | except ProgrammingError: 35 | return 36 | 37 | for check in checks: 38 | query = check.parameters[KEYS_MAPPING[check.source.plugin]] 39 | 40 | if check.type == "Threshold": 41 | threshold = check.parameters["threshold"] 42 | else: 43 | threshold = "{}:{}".format( 44 | check.parameters["bottom_threshold"], check.parameters["top_threshold"] 45 | ) 46 | 47 | params = {"query": query, "threshold": threshold} 48 | check.parameters = params 49 | db.session.commit() 50 | 51 | 52 | def downgrade(): 53 | checks = Check.query.all() 54 | for check in checks: 55 | query = check.parameters["query"] 56 | params = {KEYS_MAPPING[check.source.plugin]: query} 57 | 58 | if check.type == "Threshold": 59 | params["threshold"] = check.parameters["threshold"] 60 | else: 61 | params["bottom_threshold"], params["top_threshold"] = check.parameters[ 62 | "threshold" 63 | ].split(":") 64 | 65 | check.parameters = params 66 | db.session.commit() 67 | -------------------------------------------------------------------------------- /migrations/versions/956e9a3dd287_add_metas_column_to_teams_table.py: -------------------------------------------------------------------------------- 1 | """Add metas' column to 'teams' table 2 | 3 | Revision ID: 956e9a3dd287 4 | Revises: 30126831f6eb 5 | Create Date: 2019-06-10 15:26:59.707588 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '956e9a3dd287' 14 | down_revision = '30126831f6eb' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('teams', sa.Column('metas', sa.LargeBinary(), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('teams', 'metas') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/d2dc7ff020a0_remove_logs_table.py: -------------------------------------------------------------------------------- 1 | """Remove Logs table 2 | 3 | Revision ID: d2dc7ff020a0 4 | Revises: 48570234e11c 5 | Create Date: 2019-06-20 14:30:42.924837 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy_utils import UUIDType 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = 'd2dc7ff020a0' 15 | down_revision = '48570234e11c' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | op.drop_index(op.f('ix_logs_created_at'), table_name='logs') 22 | op.drop_table('logs') 23 | 24 | 25 | def downgrade(): 26 | op.create_table( 27 | 'logs', 28 | sa.Column('id', UUIDType(binary=False), nullable=False), 29 | sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), 30 | sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), 31 | sa.Column('level', sa.String(length=10), nullable=False), 32 | sa.Column('message', sa.String(), nullable=False), 33 | sa.Column('key', sa.String(length=50), nullable=False), 34 | sa.PrimaryKeyConstraint('id') 35 | ) 36 | op.create_index(op.f('ix_logs_created_at'), 'logs', ['created_at'], unique=False) 37 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | ; The newer versions of pytest expect registered markers or a 3 | ; PytestUnknownMarkWarning message is displayed 4 | markers = 5 | ; Currently used to skip some tests when the Neo4j database is not 6 | ; installed locally 7 | skip_requirement: Skip if requirement is not detected/installed 8 | filterwarnings = 9 | ; alembic/util/langhelpers.py:76 10 | ignore:.*inspect.getargspec.* 11 | 12 | ; werkzeug/local.py:347 13 | ignore:.*Request.is_xhr.* 14 | 15 | ; Crypto/Random/Fortuna/FortunaAccumulator.py:141 16 | ignore:.*Clock rewind detected.* 17 | 18 | ; jinja2/filters.py:24 19 | ignore:.*Flags not at the start.* 20 | 21 | ; SADeprecationWarning: Use .persist_selectable 22 | ; see: https://github.com/pallets/flask-sqlalchemy/issues/671 23 | ignore:.*.persist_selectable.* 24 | 25 | 26 | ; SADeprecationWarning: The create_engine.convert_unicode parameter 27 | ; and corresponding dialect-level parameters are deprecated. 28 | ; see: https://github.com/pallets/flask-sqlalchemy/issues/681 29 | ignore:.*create_engine.convert_unicode.* -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Core 2 | arrow==0.10.0 3 | fastjsonschema==1.6 4 | gunicorn==19.5.0 5 | jinja2==2.10.1 6 | jsonschema==3.2.0 7 | loguru==0.2.5 8 | neo4j-driver==1.6.1 9 | neo4jrestclient==2.1.1 10 | numpy==1.19.1 11 | pandas==0.23.3 12 | pycryptodome==3.7.3 13 | pyyaml==4.2b1 14 | redis==2.10.5 15 | requests==2.20.0 16 | werkzeug==0.15.4 17 | async-timeout==3.0.1 18 | aiohttp==2.3.10 19 | yarl==1.1.1 20 | 21 | # Apache Airflow 22 | apache-airflow==1.10.14 23 | 24 | # SqlAlchemy 25 | alembic==1.4.2 26 | psycopg2-binary==2.7.5 27 | sqlalchemy==1.3.0 28 | sqlalchemy-utils==0.33.10 29 | 30 | # Flask 31 | click==7.0 32 | flask==1.1.1 33 | flask-admin==1.5.4 34 | flask-cors==2.1.2 35 | flask-jsonschema==0.1.1 36 | flask-login==0.4.1 37 | flask-migrate==2.2.1 38 | flask-sqlalchemy==2.4.1 39 | 40 | # Kafka 41 | kafka-python==1.4.3 42 | 43 | # Documentation 44 | sphinx==1.8.3 45 | sphinx-rtd-theme==0.4.2 46 | sphinxcontrib-mermaid==0.3.1 47 | sphinxcontrib-httpdomain==1.7.0 48 | 49 | # Tests 50 | pytest==4.6.6 51 | deepdiff==3.3.0 52 | pytest-cov==2.6.1 53 | pytest-freezegun==0.3.0.post1 54 | pytest-mock==1.10.0 55 | -------------------------------------------------------------------------------- /scheduler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/scheduler/__init__.py -------------------------------------------------------------------------------- /scheduler/dags/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from depc import create_app 4 | 5 | 6 | app = create_app(environment=os.getenv("DEPC_ENV") or "dev") 7 | -------------------------------------------------------------------------------- /scheduler/dags/decoder.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class BoolsDpsDecoder(json.JSONDecoder): 5 | def __init__(self, *args, **kwargs): 6 | json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs) 7 | 8 | def object_hook(self, obj): 9 | if "bools_dps" not in obj: 10 | return obj 11 | obj["bools_dps"] = {int(k): v for k, v in obj["bools_dps"].items()} 12 | return obj 13 | -------------------------------------------------------------------------------- /scheduler/dags/operators/aggregation_operator.py: -------------------------------------------------------------------------------- 1 | from airflow.utils.decorators import apply_defaults 2 | 3 | from scheduler.dags.operators import DependenciesOperator 4 | 5 | 6 | class AggregationOperator(DependenciesOperator): 7 | ui_color = "#c9daf8" 8 | 9 | @apply_defaults 10 | def __init__(self, params, *args, **kwargs): 11 | super(AggregationOperator, self).__init__(params, *args, **kwargs) 12 | 13 | def compute_node_qos(self, data, start, end): 14 | from depc.utils.qos import AggregationTypes 15 | 16 | # Keep the good values 17 | data = [d["qos"] for d in data] 18 | 19 | qos = getattr(AggregationTypes, self.type)(data) 20 | return {"qos": qos} 21 | -------------------------------------------------------------------------------- /scheduler/dags/operators/average_operator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from airflow.utils.decorators import apply_defaults 3 | 4 | from scheduler.dags.operators import QosOperator 5 | 6 | 7 | class AverageOperator(QosOperator): 8 | @apply_defaults 9 | def __init__(self, params, *args, **kwargs): 10 | super(AverageOperator, self).__init__(params, *args, **kwargs) 11 | 12 | def execute(self, context): 13 | """ 14 | This task computes the average QOS for a label, using a 15 | special key in Redis populated by the chunks. 16 | """ 17 | from depc.extensions import redis_scheduler as redis 18 | from depc.utils import get_start_end_ts 19 | 20 | ds = context["ds"] 21 | start, end = get_start_end_ts(ds) 22 | 23 | # Get the list of QOS for the label 24 | key = "{ds}.{team}.{label}.sorted".format( 25 | ds=ds, team=self.team_name, label=self.label 26 | ) 27 | all_qos = [qos for _, qos in redis.zrange(key, 0, -1, withscores=True)] 28 | 29 | if not all_qos: 30 | self.log.critical("No QOS found for any {}, aborting.".format(self.label)) 31 | return 32 | 33 | self.log.info( 34 | "[{0}/{1}] Computing the average QOS using {2} items...".format( 35 | self.team_name, self.label, len(all_qos) 36 | ) 37 | ) 38 | 39 | # Let's Numpy computes the average 40 | avg_qos = np.mean(all_qos) 41 | 42 | self.log.info( 43 | "[{0}/{1}] The average QOS is {2}%".format( 44 | self.team_name, self.label, avg_qos 45 | ) 46 | ) 47 | 48 | # Saving to Beamium 49 | self.write_metric( 50 | metric="depc.qos.label", 51 | ts=start, 52 | value=avg_qos, 53 | tags={"name": self.label, "team": self.team_id}, 54 | ) 55 | -------------------------------------------------------------------------------- /scheduler/dags/operators/before_subdag_operator.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from airflow.utils.decorators import apply_defaults 4 | 5 | from depc.extensions import redis_scheduler as redis 6 | from scheduler.dags.operators import QosOperator 7 | 8 | 9 | class BeforeSubdagOperator(QosOperator): 10 | @apply_defaults 11 | def __init__(self, params, *args, **kwargs): 12 | super(BeforeSubdagOperator, self).__init__(params, *args, **kwargs) 13 | self.count = params["count"] 14 | self.excluded_nodes_from_average = params["excluded_nodes_from_average"] 15 | 16 | def execute(self, context): 17 | from depc.utils import get_start_end_ts 18 | 19 | self.logger.info( 20 | "Excluded nodes for {label}: {excluded}".format( 21 | label=self.label, excluded=self.excluded_nodes_from_average 22 | ) 23 | ) 24 | if self.excluded_nodes_from_average: 25 | redis_key = "{team}.{label}.excluded_nodes_from_label_average".format( 26 | team=self.team_name, label=self.label 27 | ) 28 | redis.delete(redis_key) 29 | redis.rpush(redis_key, *self.excluded_nodes_from_average) 30 | 31 | ds = context["ds"] 32 | start, end = get_start_end_ts(ds) 33 | 34 | # Save the total number of nodes in this label 35 | self.logger.info( 36 | "[{team}/{label}] Label contains {count} nodes".format( 37 | team=self.team_name, label=self.label, count=self.count 38 | ) 39 | ) 40 | self.write_metric( 41 | metric="depc.qos.stats", 42 | ts=start, 43 | value=self.count, 44 | tags={"team": self.team_id, "label": self.label, "type": "total"}, 45 | ) 46 | 47 | # We'll use the AfterSubdagOperator to compute 48 | # some statistics using xcom. 49 | return {"count": self.count, "start_time": time.time()} 50 | -------------------------------------------------------------------------------- /scheduler/dags/operators/daily_worst_operator.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from airflow.utils.decorators import apply_defaults 4 | 5 | from scheduler.dags.operators import QosOperator 6 | 7 | 8 | class DailyWorstOperator(QosOperator): 9 | @apply_defaults 10 | def __init__(self, params, *args, **kwargs): 11 | super(DailyWorstOperator, self).__init__(params, *args, **kwargs) 12 | 13 | def execute(self, context): 14 | from depc.controllers import NotFoundError 15 | from depc.controllers.worst import WorstController 16 | from depc.models.worst import Periods 17 | 18 | # get the right start and end with the date of the DAG 19 | ds = context["ds"] 20 | 21 | with self.app.app_context(): 22 | from depc.extensions import redis_scheduler as redis 23 | 24 | key = "{ds}.{team}.{label}.sorted".format( 25 | ds=ds, team=self.team_name, label=self.label 26 | ) 27 | 28 | start = time.time() 29 | # E.g.: [(b'filer1', 99), (b'filer3', 99.7), (b'filer2', 99.99)] 30 | data = redis.zrange( 31 | key, 0, self.app.config["MAX_WORST_ITEMS"] - 1, withscores=True 32 | ) 33 | self.log.info( 34 | "Redis ZRANGE command took {}s".format(round(time.time() - start, 3)) 35 | ) 36 | 37 | # produce the data for the relational database 38 | worst_items = {} 39 | for node, qos in data: 40 | if qos < 100: 41 | worst_items[node.decode("utf-8")] = qos 42 | 43 | nb_worst_items = len(worst_items) 44 | if nb_worst_items: 45 | self.log.info("Worst: {} item(s)".format(nb_worst_items)) 46 | 47 | metadata = { 48 | "team_id": self.team_id, 49 | "label": self.label, 50 | "period": Periods.daily, 51 | "date": ds, 52 | } 53 | 54 | try: 55 | WorstController.update( 56 | data={"data": worst_items}, filters={"Worst": metadata} 57 | ) 58 | except NotFoundError: 59 | payload = {"data": worst_items} 60 | payload.update(metadata) 61 | WorstController.create(payload) 62 | else: 63 | self.log.info("No QOS under 100%") 64 | -------------------------------------------------------------------------------- /scheduler/dags/operators/operation_operator.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from airflow.utils.decorators import apply_defaults 4 | 5 | from scheduler.dags.operators import DependenciesOperator 6 | 7 | 8 | class OperationOperator(DependenciesOperator): 9 | ui_color = "#f4cccc" 10 | 11 | @apply_defaults 12 | def __init__(self, params, *args, **kwargs): 13 | super(OperationOperator, self).__init__(params, *args, **kwargs) 14 | 15 | def compute_node_qos(self, data, start, end): 16 | from depc.utils.qos import compute_qos_from_bools, check_enable_auto_fill 17 | from depc.utils.qos import OperationTypes 18 | 19 | # Keep the good values 20 | data = [d["bools_dps"] for d in data] 21 | 22 | # RATIO and ATLEAST need an argument 23 | r = re.search(r"(.*)\((.*?)\)", self.type) 24 | if r: 25 | type = getattr(OperationTypes, r.group(1))(float(r.group(2))) 26 | else: 27 | type = getattr(OperationTypes, self.type) 28 | 29 | with self.app.app_context(): 30 | # At this point, there is no DepC rule to compute the QoS, then 31 | # the only ID provided is the team ID for check_enable_auto_fill() 32 | result = compute_qos_from_bools( 33 | booleans=data, 34 | start=start, 35 | end=end, 36 | agg_op=type, 37 | auto_fill=check_enable_auto_fill(self.team_id), 38 | ) 39 | 40 | return {"qos": result["qos"], "bools_dps": result["bools_dps"]} 41 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/tests/__init__.py -------------------------------------------------------------------------------- /tests/apiv1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/tests/apiv1/__init__.py -------------------------------------------------------------------------------- /tests/apiv1/test_ping.py: -------------------------------------------------------------------------------- 1 | def test_pong(client): 2 | resp = client.get('/v1/ping') 3 | assert resp.status_code == 200 4 | assert resp.json == {'message': 'pong'} 5 | -------------------------------------------------------------------------------- /tests/consumer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/tests/consumer/__init__.py -------------------------------------------------------------------------------- /tests/controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/tests/controllers/__init__.py -------------------------------------------------------------------------------- /tests/controllers/test_teams.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from depc.controllers.teams import TeamController 4 | 5 | 6 | def test_generate_marmaid_diagram_simple(app): 7 | configs = {"Apache": {"qos": "rule.Servers"}} 8 | 9 | with app.app_context(): 10 | query = TeamController()._generate_marmaid_diagram(configs) 11 | 12 | assert query == "graph TB\\n\\ndepc.qos.label_name_Apache_[Apache]\\n" 13 | 14 | 15 | def test_generate_marmaid_diagram_advanced(app): 16 | configs = { 17 | "Apache": {"qos": "rule.Servers"}, 18 | "Filer": {"qos": "rule.Servers"}, 19 | "Offer": {"qos": "aggregation.AVERAGE[Website]"}, 20 | "Website": {"qos": "operation.AND[Filer, Apache]"} 21 | } 22 | 23 | with app.app_context(): 24 | query = TeamController()._generate_marmaid_diagram(configs) 25 | 26 | assert "graph TB\\n\\n" in query 27 | assert "depc.qos.label_name_Filer_[Filer]\\n" in query 28 | assert "depc.qos.label_name_Apache_[Apache]\\n" in query 29 | assert "depc.qos.label_name_Website_[Website] --> depc.qos.label_name_Filer_[Filer]\\n" in query 30 | assert "depc.qos.label_name_Website_[Website] --> depc.qos.label_name_Apache_[Apache]\\n" in query 31 | assert "depc.qos.label_name_Offer_[Offer] --> depc.qos.label_name_Website_[Website]\\n" in query 32 | -------------------------------------------------------------------------------- /tests/data/available_sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "sources": [ 3 | { 4 | "description": "Fake source used in the documentation tutorial.", 5 | "form": [], 6 | "name": "Fake", 7 | "schema": { 8 | "properties": {}, 9 | "type": "object" 10 | } 11 | }, 12 | { 13 | "description": "Use a database Warp10 to launch your WarpScripts.", 14 | "form": [ 15 | { 16 | "key": "url", 17 | "placeholder": "http://127.0.0.1" 18 | }, 19 | { 20 | "key": "token", 21 | "placeholder": "foobar" 22 | } 23 | ], 24 | "name": "WarpScript", 25 | "schema": { 26 | "properties": { 27 | "token": { 28 | "description": "A read only token used authenticate the scripts.", 29 | "title": "Token", 30 | "type": "string" 31 | }, 32 | "url": { 33 | "description": "The url used to launch the scripts.", 34 | "title": "Url", 35 | "type": "string" 36 | } 37 | }, 38 | "required": [ 39 | "url", 40 | "token" 41 | ], 42 | "type": "object" 43 | } 44 | }, 45 | { 46 | "description": "Use an OpenTSDB database to launch your queries.", 47 | "form": [ 48 | { 49 | "key": "url", 50 | "placeholder": "http://127.0.0.1" 51 | }, 52 | { 53 | "key": "credentials", 54 | "placeholder": "foo:bar" 55 | } 56 | ], 57 | "name": "OpenTSDB", 58 | "schema": { 59 | "properties": { 60 | "credentials": { 61 | "description": "The credentials used authenticate the queries.", 62 | "title": "Credentials", 63 | "type": "string" 64 | }, 65 | "url": { 66 | "description": "The url used to query the database.", 67 | "title": "Url", 68 | "type": "string" 69 | } 70 | }, 71 | "required": [ 72 | "url", 73 | "credentials" 74 | ], 75 | "type": "object" 76 | } 77 | } 78 | ] 79 | } -------------------------------------------------------------------------------- /tests/data/opentsdb_source.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Use an OpenTSDB database to launch your queries.", 3 | "form": [ 4 | { 5 | "key": "url", 6 | "placeholder": "http://127.0.0.1" 7 | }, 8 | { 9 | "key": "credentials", 10 | "placeholder": "foo:bar" 11 | } 12 | ], 13 | "schema": { 14 | "properties": { 15 | "credentials": { 16 | "description": "The credentials used authenticate the queries.", 17 | "title": "Credentials", 18 | "type": "string" 19 | }, 20 | "url": { 21 | "description": "The url used to query the database.", 22 | "title": "Url", 23 | "type": "string" 24 | } 25 | }, 26 | "required": [ 27 | "url", 28 | "credentials" 29 | ], 30 | "type": "object" 31 | } 32 | } -------------------------------------------------------------------------------- /tests/data/opentsdb_threshold_check.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "This check executes a query on an OpenTSDB : every datapoints which\nis above a critical threshold lower the QOS.", 3 | "form": [ 4 | { 5 | "key": "query", 6 | "type": "codemirror" 7 | }, 8 | { 9 | "key": "threshold", 10 | "placeholder": "Ex: 500" 11 | } 12 | ], 13 | "schema": { 14 | "additionalProperties": false, 15 | "properties": { 16 | "query": { 17 | "description": "Query must return 1 or more timeserie(s).", 18 | "title": "OpenTSDB query", 19 | "type": "string" 20 | }, 21 | "threshold": { 22 | "description": "The QOS will be lowered for every values strictly superior to this threshold.", 23 | "title": "Threshold", 24 | "type": "string" 25 | } 26 | }, 27 | "required": [ 28 | "query", 29 | "threshold" 30 | ], 31 | "type": "object" 32 | } 33 | } -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/tests/utils/__init__.py -------------------------------------------------------------------------------- /ui/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /ui/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.15.10 2 | 3 | WORKDIR /app 4 | 5 | RUN apt-get update \ 6 | && apt-get -y install curl gnupg2 apt-transport-https git 7 | 8 | # Install Node.js 8 9 | RUN curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key -o nodesource.gpg.key \ 10 | && apt-key add nodesource.gpg.key 11 | RUN echo 'deb https://deb.nodesource.com/node_8.x stretch main' > /etc/apt/sources.list.d/nodesource.list 12 | RUN apt-get update \ 13 | && apt-get install -y nodejs 14 | 15 | # Install npm packages 16 | COPY package.json /app/package.json 17 | 18 | RUN npm install \ 19 | && npm install bower -g \ 20 | && npm install grunt-cli -g \ 21 | && npm install grunt -g 22 | 23 | # Install bower components 24 | COPY bower.json /app/bower.json 25 | 26 | RUN bower install --allow-root 27 | 28 | # Copy the source fles 29 | COPY app app 30 | 31 | # Build the app 32 | COPY Gruntfile.js /app/Gruntfile.js 33 | 34 | # Build HTML/CSS/JS 35 | RUN grunt build \ 36 | && mv /app/dist/* /usr/share/nginx/html 37 | 38 | # Start web server 39 | EXPOSE 80 40 | CMD ["/usr/sbin/nginx", "-g", "daemon off;"] 41 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # depcwebui 2 | 3 | This project is generated with [yo angular generator](https://github.com/yeoman/generator-angular) 4 | version 0.15.1. 5 | 6 | ## Build & development 7 | 8 | Run `grunt` for building and `grunt serve` for preview. 9 | 10 | -------------------------------------------------------------------------------- /ui/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/favicon.ico -------------------------------------------------------------------------------- /ui/app/images/alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/alert.png -------------------------------------------------------------------------------- /ui/app/images/bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/bg.gif -------------------------------------------------------------------------------- /ui/app/images/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/empty.png -------------------------------------------------------------------------------- /ui/app/images/fake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/fake.png -------------------------------------------------------------------------------- /ui/app/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/favicon-16x16.png -------------------------------------------------------------------------------- /ui/app/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/favicon-32x32.png -------------------------------------------------------------------------------- /ui/app/images/gray-line-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/gray-line-bg.gif -------------------------------------------------------------------------------- /ui/app/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/loading.gif -------------------------------------------------------------------------------- /ui/app/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/logo.png -------------------------------------------------------------------------------- /ui/app/images/opentsdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/opentsdb.png -------------------------------------------------------------------------------- /ui/app/images/panel-critical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/panel-critical.png -------------------------------------------------------------------------------- /ui/app/images/panel-ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/panel-ok.png -------------------------------------------------------------------------------- /ui/app/images/panel-unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/panel-unknown.png -------------------------------------------------------------------------------- /ui/app/images/panel-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/panel-warning.png -------------------------------------------------------------------------------- /ui/app/images/warp10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/depc/10d921f23c455ba0da88c2d1198f25ce4fad0e6c/ui/app/images/warp10.png -------------------------------------------------------------------------------- /ui/app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | Disallow: 5 | -------------------------------------------------------------------------------- /ui/app/scripts/controllers/display_check_parameters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name depcwebuiApp.controller:ModalDisplayCheckParametersCtrl 6 | * @description 7 | * # ModalDisplayCheckParametersCtrl 8 | * Controller of the depcwebuiApp 9 | */ 10 | angular.module('depcwebuiApp') 11 | .controller('ModalDisplayCheckParametersCtrl', function ($uibModalInstance, check) { 12 | var self = this; 13 | self.check = check; 14 | }); -------------------------------------------------------------------------------- /ui/app/scripts/controllers/modal_associate_checks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name depcwebuiApp.controller:ModalAssociateChecksCtrl 6 | * @description 7 | * # ModalAssociateChecksCtrl 8 | * Controller of the depcwebuiApp 9 | */ 10 | angular.module('depcwebuiApp') 11 | .controller('ModalAssociateChecksCtrl', function ($uibModalInstance, teamsService, sourcesService, rulesService, toastr, team, rule) { 12 | var self = this; 13 | 14 | // Init variables 15 | self.team = team; 16 | self.rule = rule; 17 | self.sources = []; 18 | self.activatedChecks = []; 19 | self.loader = true; 20 | 21 | sourcesService.getTeamSources(self.team.id).then(function(response) { 22 | self.sources = response.data.sources; 23 | 24 | for ( var check in self.rule.checks ) { 25 | self.activatedChecks.push(self.rule.checks[check].id); 26 | } 27 | self.loader = false; 28 | }); 29 | 30 | this.addCheck = function(check) { 31 | var idx = self.activatedChecks.indexOf(check.id); 32 | 33 | if (idx > -1) { 34 | self.activatedChecks.splice(idx, 1); 35 | } else { 36 | self.activatedChecks.push(check.id); 37 | } 38 | }; 39 | 40 | this.apply = function() { 41 | if ( self.activatedChecks ) { 42 | rulesService.associateRuleChecks(self.team.id, self.rule.id, self.activatedChecks).then(function(response) { 43 | toastr.success('Checks have been updated.'); 44 | $uibModalInstance.close(response.data); 45 | }); 46 | } 47 | }; 48 | 49 | this.cancel = function () { 50 | $uibModalInstance.dismiss('cancel'); 51 | }; 52 | 53 | }); -------------------------------------------------------------------------------- /ui/app/scripts/controllers/modal_display_config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name depcwebuiApp.controller:ModalDisplayConfigCtrl 6 | * @description 7 | * # ModalDisplayConfigCtrl 8 | * Controller of the depcwebuiApp 9 | */ 10 | angular.module('depcwebuiApp') 11 | .controller('ModalDisplayConfigCtrl', function ($uibModalInstance, $confirm, toastr, configurationsService, team, config) { 12 | var self = this; 13 | 14 | // Init variables 15 | self.team = team; 16 | self.config = config; 17 | self.jsonConfig = JSON.stringify(self.config.data, null, ' '); 18 | self.cmOption = { 19 | lineNumbers: false, 20 | theme: 'twilight', 21 | mode: 'javascript', 22 | readOnly: true 23 | }; 24 | 25 | this.cancel = function () { 26 | $uibModalInstance.dismiss('cancel'); 27 | }; 28 | 29 | this.revert = function() { 30 | $confirm({ 31 | text: 'Are you sure you want to revert the "' + self.config.created_at + '" version ?', 32 | title: 'Revert the configuration', 33 | ok: 'Yes', 34 | cancel: 'No' 35 | }) 36 | .then(function() { 37 | configurationsService.revertTeamConfiguration(self.team.id, self.config.id).then(function(response) { 38 | toastr.success('The configuration has been reverted.'); 39 | $uibModalInstance.close(response.data); 40 | }); 41 | }); 42 | }; 43 | }); -------------------------------------------------------------------------------- /ui/app/scripts/controllers/modal_display_json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name depcwebuiApp.controller:ModalDisplayJsonCtrl 6 | * @description 7 | * # ModalDisplayJsonCtrl 8 | * Controller of the depcwebuiApp 9 | */ 10 | angular.module('depcwebuiApp') 11 | .controller('ModalDisplayJsonCtrl', function (title, description, json) { 12 | var self = this; 13 | 14 | self.title = title; 15 | self.description = description; 16 | self.json = JSON.stringify(json, null, ' '); 17 | 18 | self.cmOption = { 19 | lineNumbers: false, 20 | theme: 'twilight', 21 | mode: 'javascript', 22 | readOnly: true 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /ui/app/scripts/controllers/modal_edit_rule.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name depcwebuiApp.controller:ModalEditRuleCtrl 6 | * @description 7 | * # ModalEditRuleCtrl 8 | * Controller of the depcwebuiApp 9 | */ 10 | angular.module('depcwebuiApp') 11 | .controller('ModalEditRuleCtrl', function ($uibModalInstance, teamsService, toastr, team, rule) { 12 | var self = this; 13 | 14 | // Init variables 15 | self.team = team; 16 | self.rule = rule; 17 | self.name = rule.name; 18 | self.description = rule.description; 19 | 20 | this.cancel = function () { 21 | $uibModalInstance.dismiss('cancel'); 22 | }; 23 | 24 | this.edit = function() { 25 | if ( self.name ) { 26 | teamsService.editTeamRule(self.team.id, self.rule.id, self.name, self.description).then(function(response) { 27 | toastr.success('Rule ' + self.name + ' has been edited.'); 28 | $uibModalInstance.close(response.data); 29 | }); 30 | } 31 | }; 32 | }); -------------------------------------------------------------------------------- /ui/app/scripts/controllers/modal_edit_source.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name depcwebuiApp.controller:ModalEditSourceCtrl 6 | * @description 7 | * # ModalEditSourceCtrl 8 | * Controller of the depcwebuiApp 9 | */ 10 | angular.module('depcwebuiApp') 11 | .controller('ModalEditSourceCtrl', function ($uibModalInstance, sourcesService, toastr, team, source) { 12 | var self = this; 13 | 14 | // Init variables 15 | self.team = team; 16 | self.source = source; 17 | self.name = source.name; 18 | self.configuration = source.configuration; 19 | 20 | sourcesService.getAvailableSourceInfo(source.plugin).then(function(response) { 21 | self.plugin = response.data; 22 | }); 23 | 24 | this.cancel = function () { 25 | $uibModalInstance.dismiss('cancel'); 26 | }; 27 | 28 | this.edit = function() { 29 | sourcesService.editTeamSource(self.team.id, self.source.id, self.name, self.plugin.name, self.configuration).then(function(response) { 30 | toastr.success('Source ' + self.name + ' has been edited.'); 31 | $uibModalInstance.close(response.data); 32 | }); 33 | }; 34 | }); -------------------------------------------------------------------------------- /ui/app/scripts/controllers/modal_new_config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name depcwebuiApp.controller:ModalNewConfigCtrl 6 | * @description 7 | * # ModalNewConfigCtrl 8 | * Controller of the depcwebuiApp 9 | */ 10 | angular.module('depcwebuiApp') 11 | .controller('ModalNewConfigCtrl', function ($uibModalInstance, $confirm, toastr, configurationsService, team, placeholder) { 12 | var self = this; 13 | 14 | // Init variables 15 | self.team = team; 16 | self.cmOption = { 17 | lineNumbers: false, 18 | theme: 'twilight', 19 | mode: 'javascript' 20 | }; 21 | 22 | // We prefill the configuration with the placeholder 23 | self.config = JSON.stringify(placeholder, null, ' '); 24 | 25 | self.cancel = function () { 26 | $uibModalInstance.dismiss('cancel'); 27 | }; 28 | 29 | 30 | self.save = function() { 31 | $confirm({ 32 | text: 'This configuration will remove the previous one (if exists). Are you sure ?', 33 | title: 'Save the configuration', 34 | ok: 'Yes', 35 | cancel: 'No' 36 | }) 37 | .then(function() { 38 | try { 39 | var config = JSON.parse(self.config); 40 | } catch (e) { 41 | toastr.error('Your configuration is not valid (must be a JSON object).'); 42 | return false; 43 | } 44 | configurationsService.createTeamConfiguration(self.team.id, config).then(function(response) { 45 | toastr.success('The configuration has been saved.'); 46 | $uibModalInstance.close(response.data); 47 | }); 48 | }); 49 | }; 50 | }); 51 | -------------------------------------------------------------------------------- /ui/app/scripts/controllers/modal_new_rule.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name depcwebuiApp.controller:ModalNewRuleCtrl 6 | * @description 7 | * # ModalNewRuleCtrl 8 | * Controller of the depcwebuiApp 9 | */ 10 | angular.module('depcwebuiApp') 11 | .controller('ModalNewRuleCtrl', function ($uibModalInstance, teamsService, toastr, team) { 12 | var self = this; 13 | 14 | // Init variables 15 | self.name = null; 16 | self.description = null; 17 | self.team = team; 18 | 19 | this.cancel = function () { 20 | $uibModalInstance.dismiss('cancel'); 21 | }; 22 | 23 | this.create = function() { 24 | if ( self.name ) { 25 | teamsService.createTeamRule(self.team.id, self.name, self.description).then(function(response) { 26 | toastr.success('Rule ' + self.name + ' has been created.'); 27 | $uibModalInstance.close(response.data); 28 | }); 29 | } 30 | }; 31 | }); -------------------------------------------------------------------------------- /ui/app/scripts/controllers/modal_new_source.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name depcwebuiApp.controller:ModalNewSourceCtrl 6 | * @description 7 | * # ModalNewSourceCtrl 8 | * Controller of the depcwebuiApp 9 | */ 10 | angular.module('depcwebuiApp') 11 | .controller('ModalNewSourceCtrl', function ($uibModalInstance, sourcesService, toastr, team) { 12 | var self = this; 13 | 14 | // Init variables 15 | self.team = team; 16 | self.name = null; 17 | self.plugin = null; 18 | self.configuration = {}; 19 | 20 | sourcesService.getAvailableSources().then(function(response) { 21 | self.availablePlugins = response.data.sources; 22 | }); 23 | 24 | this.cancel = function () { 25 | $uibModalInstance.dismiss('cancel'); 26 | }; 27 | 28 | this.create = function() { 29 | sourcesService.createTeamSource(self.team.id, self.name, self.plugin.name, self.configuration).then(function(response) { 30 | toastr.success('Source ' + self.name + ' has been created.'); 31 | $uibModalInstance.close(response.data); 32 | }); 33 | }; 34 | }); -------------------------------------------------------------------------------- /ui/app/scripts/controllers/modal_news.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name depcwebuiApp.controller:ModalNewsCtrl 6 | * @description 7 | * # ModalNewsCtrl 8 | * Controller of the depcwebuiApp 9 | */ 10 | angular.module('depcwebuiApp') 11 | .controller('ModalNewsCtrl', function ($rootScope, newsService) { 12 | var self = this; 13 | 14 | self.title = "Latest news"; 15 | self.news = $rootScope.globals.news; 16 | 17 | self.displayAllNews = function() { 18 | self.title = "All news"; 19 | newsService.getAllNews().then(function(response) { 20 | self.news = response.data.news; 21 | }); 22 | }; 23 | }); 24 | -------------------------------------------------------------------------------- /ui/app/scripts/controllers/modal_relationship_periods.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name depcwebuiApp.controller:ModalRelationshipPeriodsCtrl 6 | * @description 7 | * # ModalRelationshipPeriodsCtrl 8 | * Controller of the depcwebuiApp 9 | */ 10 | angular.module('depcwebuiApp') 11 | .controller('ModalRelationshipPeriodsCtrl', function ($filter, periods) { 12 | var self = this; 13 | 14 | // Transform the array into list of dict 15 | var groupedPeriods = []; 16 | var pushed = false; 17 | 18 | for ( var i in periods ) { 19 | if ( i % 2 == 0 ) { 20 | var bar = {'from': periods[i]}; 21 | pushed = false; 22 | } else { 23 | bar['to'] = periods[i] 24 | groupedPeriods.push(bar); 25 | pushed = true; 26 | } 27 | } 28 | 29 | // Last state can be 'from' 30 | if ( !pushed ) { 31 | groupedPeriods.push(bar); 32 | } 33 | self.periods = groupedPeriods; 34 | 35 | self.getNodeDate = function(d) { 36 | if (!d) { 37 | return '--' 38 | } 39 | return $filter('date')(d * 1000, 'yyyy-MM-dd HH:mm:ss') 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /ui/app/scripts/controllers/team/permissions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name depcwebuiApp.controller:PermissionsCtrl 6 | * @description 7 | * # PermissionsCtrl 8 | * Controller of the depcwebuiApp 9 | */ 10 | angular.module('depcwebuiApp') 11 | .controller('PermissionsCtrl', function ($routeParams, $scope, toastr, teamsService, usersService) { 12 | var self = this; 13 | 14 | self.teamName = $routeParams.team; 15 | self.search = ''; 16 | self.load = true; 17 | self.includeOther = false; 18 | 19 | this.refresh = function() { 20 | self.load = true; 21 | self.users = []; 22 | self.grants = []; 23 | 24 | teamsService.getTeamByName(self.teamName).then(function(response) { 25 | self.team = response.data; 26 | 27 | usersService.getUsers().then(function(response) { 28 | var users = response.data.users; 29 | 30 | teamsService.getTeamGrants(self.team.id).then(function(response) { 31 | var grants = response.data; 32 | var grantedUsers = []; 33 | for ( var grant in grants ) { 34 | grantedUsers.push(grants[grant].user); 35 | self.grants[grants[grant]['user']] = grants[grant]['role']; 36 | }; 37 | 38 | if ( self.includeOther ) { 39 | self.users = users; 40 | } else { 41 | var users_copy = [] 42 | for ( var user in users ) { 43 | if ( grantedUsers.indexOf(users[user].name) > -1 ) { 44 | users_copy.push(users[user]); 45 | } 46 | } 47 | self.users = users_copy; 48 | } 49 | 50 | self.load = false; 51 | }); 52 | }); 53 | }); 54 | }; 55 | 56 | // Load the data 57 | self.refresh(); 58 | $scope.$watch(function() { 59 | return self.includeOther; 60 | }, function() { 61 | self.refresh(); 62 | }, true); 63 | 64 | this.applyGrants = function() { 65 | self.load = true; 66 | 67 | var grants = []; 68 | for ( var grant in self.grants ) { 69 | grants.push({ 70 | 'user': grant, 71 | 'role': self.grants[grant] 72 | }); 73 | }; 74 | 75 | teamsService.associateTeamGrants(self.team.id, grants).then(function(response) { 76 | if ( response.status == 200 ) { 77 | self.refresh(); 78 | toastr.success('Grants have been updated.'); 79 | } 80 | }); 81 | 82 | }; 83 | 84 | this.fillSearch = function(username) { 85 | self.search = username; 86 | } 87 | }); -------------------------------------------------------------------------------- /ui/app/scripts/controllers/teams.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name depcwebuiApp.controller:TeamsCtrl 6 | * @description 7 | * # TeamsCtrl 8 | * Controller of the depcwebuiApp 9 | */ 10 | angular.module('depcwebuiApp') 11 | .controller('TeamsCtrl', function (teamsService, usersService, config, modalService) { 12 | var self = this; 13 | 14 | self.teams = []; 15 | self.teamsLoading = true; 16 | teamsService.getTeams().then(function(response) { 17 | var teams = response.data.teams; 18 | 19 | // Be sure user.grants exists before using it 20 | usersService.getCurrentUser().then(function(response) { 21 | var grants = response.data.grants; 22 | 23 | // Group team (memberOf or not) 24 | var data = { 25 | 'userOf': { 26 | 'title': 'Your teams', 27 | 'teams': [] 28 | }, 29 | 'notUserOf': { 30 | 'title': 'Other teams', 31 | 'teams': [] 32 | } 33 | }; 34 | for ( var i in teams ) { 35 | if ( teams[i].name in grants ) { 36 | data.userOf.teams.push(teams[i]); 37 | } else { 38 | data.notUserOf.teams.push(teams[i]); 39 | } 40 | } 41 | 42 | self.teams = data; 43 | self.teamsLoading = false; 44 | }); 45 | }); 46 | 47 | this.displayGrants = function(team) { 48 | modalService.displayGrants(team); 49 | }; 50 | 51 | this.countTotalChecks = function(team) { 52 | var count = 0; 53 | for ( var rule in team.rules ) { 54 | count += team.rules[rule].checks.length; 55 | } 56 | return config.pluralize(count, 'check', 'checks', true); 57 | }; 58 | 59 | this.displayManagers = function(team) { 60 | teamsService.getTeamGrants(team.id).then(function(response) { 61 | var grants = []; 62 | 63 | // Keep the managers 64 | for ( var i in response.data ) { 65 | if ( response.data[i].role == "manager" ) { 66 | grants.push(response.data[i].user); 67 | } 68 | } 69 | 70 | modalService.displayJson( 71 | "List of managers", 72 | "
    Contact one of this people if you want to join the team.
    ", 73 | grants 74 | ); 75 | }); 76 | }; 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /ui/app/scripts/filters/numberTrunc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * @ngdoc filter 6 | * @name depcwebuiApp.filter:numberTrunc 7 | * @function 8 | * @description 9 | * # numberTrunc 10 | * Filter in the depcwebuiApp. 11 | */ 12 | angular.module('depcwebuiApp') 13 | .filter('numberTrunc', function () { 14 | return function (num, precision) { 15 | 16 | if ( num == undefined || num == null || isNaN(num) ) { 17 | return null; 18 | } 19 | // javascript's floating point math function are hazardous 20 | // so we manipulate the string representation instead. 21 | // see: https://stackoverflow.com/a/11818658/956660 22 | var re = new RegExp('^-?\\d+(?:\.\\d{0,' + (precision || -1) + '})?'); 23 | return num.toString().match(re)[0]; 24 | }; 25 | } 26 | ); 27 | -------------------------------------------------------------------------------- /ui/app/scripts/filters/slugify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc filter 5 | * @name depcwebuiApp.filter:slugify 6 | * @function 7 | * @description 8 | * # slugify 9 | * Filter in the depcwebuiApp. 10 | */ 11 | angular.module('depcwebuiApp') 12 | .filter('slugify', function () { 13 | return function (input) { 14 | if (!input) { 15 | return; 16 | } 17 | 18 | // make lower case and trim 19 | var slug = input.toLowerCase().trim(); 20 | 21 | // replace invalid chars with spaces 22 | slug = slug.replace(/[^a-z0-9\s-]/g, ' '); 23 | 24 | // replace multiple spaces or hyphens with a single hyphen 25 | slug = slug.replace(/[\s-]+/g, '-'); 26 | 27 | return slug; 28 | }; 29 | }); 30 | -------------------------------------------------------------------------------- /ui/app/scripts/services/breadcrumbs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc service 5 | * @name depcwebuiApp.breadcrumbs 6 | * @description 7 | * # breadcrumbs 8 | * Service in the depcwebuiApp. 9 | */ 10 | angular.module('depcwebuiApp') 11 | .service('breadcrumbs', function ($rootScope, $location, $route) { 12 | 13 | var breadcrumbs = [], 14 | breadcrumbsService = {}, 15 | routes = $route.routes; 16 | 17 | var generateBreadcrumbs = function() { 18 | breadcrumbs = []; 19 | var pathElements = $location.path().split('/'), 20 | path = ''; 21 | 22 | var getRoute = function(route) { 23 | angular.forEach($route.current.params, function(value, key) { 24 | var re = new RegExp(value); 25 | route = route.replace(re, ':' + key); 26 | }); 27 | return route; 28 | }; 29 | if (pathElements[1] == '') delete pathElements[1]; 30 | angular.forEach(pathElements, function(el) { 31 | path += path === '/' ? el : '/' + el; 32 | var route = getRoute(path); 33 | 34 | // The following url uses the wildcard to handle names 35 | // containing a "/", we need to transform it. 36 | if (route == '/teams/:team/dashboard/:label/:name') { 37 | route = '/teams/:team/dashboard/:label/:name*' 38 | } 39 | 40 | if (routes[route] && routes[route].label) { 41 | var label = routes[route].label; 42 | if ( label.startsWith(':') ) { 43 | label = label.substring(1) 44 | label = $route.current.params[label] 45 | } 46 | breadcrumbs.push({ label: label, path: path }); 47 | } 48 | }); 49 | }; 50 | 51 | // We want to update breadcrumbs only when a route is actually changed 52 | // as $location.path() will get updated immediately (even if route change fails!) 53 | $rootScope.$on('$routeChangeSuccess', function(event, current) { 54 | generateBreadcrumbs(); 55 | }); 56 | 57 | breadcrumbsService.getAll = function() { 58 | return breadcrumbs; 59 | }; 60 | 61 | breadcrumbsService.getFirst = function() { 62 | return breadcrumbs[0] || {}; 63 | }; 64 | 65 | return breadcrumbsService; 66 | }); 67 | -------------------------------------------------------------------------------- /ui/app/scripts/services/checks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc service 5 | * @name depcwebuiApp.checks 6 | * @description 7 | * # checks 8 | * Service in the depcwebuiApp. 9 | */ 10 | angular.module('depcwebuiApp') 11 | .service('checksService', function ($http, config) { 12 | 13 | var getTeamChecks = function(team_id) { 14 | return $http({ 15 | url: config.depc_endpoint() + '/teams/' + team_id + '/checks', 16 | method: "GET" 17 | }); 18 | }; 19 | 20 | return { 21 | getTeamChecks: getTeamChecks 22 | }; 23 | }); 24 | -------------------------------------------------------------------------------- /ui/app/scripts/services/configurations.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc service 5 | * @name depcwebuiApp.configurations 6 | * @description 7 | * # configurations 8 | * Service in the depcwebuiApp. 9 | */ 10 | angular.module('depcwebuiApp') 11 | .service('configurationsService', function ($http, config) { 12 | 13 | var getTeamConfigurations = function(team_id) { 14 | return $http({ 15 | url: config.depc_endpoint() + '/teams/' + team_id + '/configs', 16 | method: "GET" 17 | }); 18 | }; 19 | 20 | var getTeamCurrentConfiguration = function(team_id) { 21 | return $http({ 22 | url: config.depc_endpoint() + '/teams/' + team_id + '/configs/current', 23 | method: "GET" 24 | }); 25 | }; 26 | 27 | var revertTeamConfiguration = function(team_id, config_id) { 28 | return $http({ 29 | url: config.depc_endpoint() + '/teams/' + team_id + '/configs/' + config_id + '/apply', 30 | method: "PUT", 31 | data: {} 32 | }); 33 | }; 34 | 35 | var createTeamConfiguration = function(team_id, conf) { 36 | return $http({ 37 | url: config.depc_endpoint() + '/teams/' + team_id + '/configs', 38 | method: "POST", 39 | data: conf 40 | }); 41 | }; 42 | 43 | return { 44 | getTeamConfigurations: getTeamConfigurations, 45 | getTeamCurrentConfiguration: getTeamCurrentConfiguration, 46 | revertTeamConfiguration: revertTeamConfiguration, 47 | createTeamConfiguration: createTeamConfiguration 48 | } 49 | }); -------------------------------------------------------------------------------- /ui/app/scripts/services/httpinterceptor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc service 5 | * @name depcwebuiApp.httpInterceptor 6 | * @description 7 | * # httpInterceptor 8 | * Service in the depcwebuiApp. 9 | */ 10 | angular.module('depcwebuiApp') 11 | .service('httpInterceptor', function ($rootScope, $q) { 12 | 13 | return { 14 | 'responseError': function(rejection) { 15 | $rootScope.$broadcast('requestError', rejection.data); 16 | return $q.reject(rejection); 17 | }, 18 | 'request': function(config) { 19 | // config.headers['X-Remote-User'] = 'username'; 20 | return config; 21 | } 22 | }; 23 | }); 24 | 25 | -------------------------------------------------------------------------------- /ui/app/scripts/services/news.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc service 5 | * @name depcwebuiApp.news 6 | * @description 7 | * # news 8 | * Service in the depcwebuiApp. 9 | */ 10 | angular.module('depcwebuiApp') 11 | .service('newsService', function ($http, config) { 12 | 13 | var getUnreadNews = function() { 14 | return $http({ 15 | url: config.depc_endpoint() + '/news?unread=1', 16 | method: "GET" 17 | }); 18 | }; 19 | 20 | var getAllNews = function() { 21 | return $http({ 22 | url: config.depc_endpoint() + '/news', 23 | method: "GET" 24 | }); 25 | }; 26 | 27 | var clear = function() { 28 | return $http({ 29 | url: config.depc_endpoint() + '/news', 30 | method: "DELETE" 31 | }); 32 | }; 33 | 34 | return { 35 | getUnreadNews: getUnreadNews, 36 | getAllNews: getAllNews, 37 | clear: clear 38 | }; 39 | }); 40 | -------------------------------------------------------------------------------- /ui/app/scripts/services/rules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc service 5 | * @name depcwebuiApp.rules 6 | * @description 7 | * # rules 8 | * Service in the depcwebuiApp. 9 | */ 10 | angular.module('depcwebuiApp') 11 | .service('rulesService', function ($http, config) { 12 | 13 | var getTeamRules = function(team_id) { 14 | return $http({ 15 | url: config.depc_endpoint() + '/teams/' + team_id + '/rules', 16 | method: "GET" 17 | }); 18 | }; 19 | 20 | var getCheck = function(check_id) { 21 | return $http({ 22 | url: config.depc_endpoint() + '/checks/' + check_id, 23 | method: "GET" 24 | }); 25 | }; 26 | 27 | var executeRule = function(team_id, rule_or_label, name, start, end) { 28 | var name = name || ""; 29 | var data = { 30 | 'name': name, 31 | 'start': start, 32 | 'end': end 33 | }; 34 | 35 | return $http({ 36 | url: config.depc_endpoint() + '/teams/' + team_id + '/rules/' + rule_or_label + '/execute', 37 | method: "POST", 38 | data: data 39 | }); 40 | }; 41 | 42 | var associateRuleChecks = function(team_id, rule_id, checks) { 43 | return $http({ 44 | url: config.depc_endpoint() + '/teams/' + team_id + '/rules/' + rule_id + '/checks', 45 | method: "PUT", 46 | data: { 47 | "checks": checks 48 | } 49 | }); 50 | }; 51 | 52 | var removeRuleLabel = function(team_id, rule_id, label) { 53 | return $http({ 54 | url: config.depc_endpoint() + '/teams/' + team_id + '/rules/' + rule_id + '/labels/' + label, 55 | method: "DELETE" 56 | }); 57 | }; 58 | 59 | return { 60 | getTeamRules: getTeamRules, 61 | executeRule: executeRule, 62 | getCheck: getCheck, 63 | associateRuleChecks: associateRuleChecks, 64 | removeRuleLabel: removeRuleLabel 65 | }; 66 | }); 67 | -------------------------------------------------------------------------------- /ui/app/scripts/services/statistics.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc service 5 | * @name depcwebuiApp.statistics 6 | * @description 7 | * # statistics 8 | * Service in the depcwebuiApp. 9 | */ 10 | angular.module('depcwebuiApp') 11 | .service('statisticsService', function ($http, config) { 12 | 13 | var getTeamStatistics = function(team_id, sort, start, end) { 14 | return $http({ 15 | url: config.depc_endpoint() + '/teams/' + team_id + '/statistics?sort=' + sort + '&start=' + start + '&end=' + end, 16 | method: "GET" 17 | }); 18 | }; 19 | 20 | return { 21 | getTeamStatistics: getTeamStatistics 22 | } 23 | }); -------------------------------------------------------------------------------- /ui/app/scripts/services/users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc service 5 | * @name depcwebuiApp.users 6 | * @description 7 | * # users 8 | * Service in the depcwebuiApp. 9 | */ 10 | angular.module('depcwebuiApp') 11 | .service('usersService', function ($http, config) { 12 | 13 | var getCurrentUser = function() { 14 | return $http({ 15 | url: config.depc_endpoint() + '/users/me', 16 | method: "GET" 17 | }); 18 | }; 19 | 20 | var getUsers = function() { 21 | return $http({ 22 | url: config.depc_endpoint() + '/users', 23 | method: "GET" 24 | }); 25 | }; 26 | 27 | return { 28 | getCurrentUser: getCurrentUser, 29 | getUsers: getUsers 30 | }; 31 | }); -------------------------------------------------------------------------------- /ui/app/views/modals/associate_checks.html: -------------------------------------------------------------------------------- 1 | 4 | 26 | 30 | -------------------------------------------------------------------------------- /ui/app/views/modals/confirm.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | -------------------------------------------------------------------------------- /ui/app/views/modals/display_check_parameters.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /ui/app/views/modals/display_check_result.html: -------------------------------------------------------------------------------- 1 | 10 | 31 | -------------------------------------------------------------------------------- /ui/app/views/modals/display_config.html: -------------------------------------------------------------------------------- 1 | 4 | 7 | 11 | -------------------------------------------------------------------------------- /ui/app/views/modals/display_json.html: -------------------------------------------------------------------------------- 1 | 4 | 8 | -------------------------------------------------------------------------------- /ui/app/views/modals/edit_rule.html: -------------------------------------------------------------------------------- 1 | 4 | 20 | 24 | -------------------------------------------------------------------------------- /ui/app/views/modals/edit_source.html: -------------------------------------------------------------------------------- 1 | 4 | 26 | 30 | -------------------------------------------------------------------------------- /ui/app/views/modals/new_config.html: -------------------------------------------------------------------------------- 1 | 4 | 9 | 13 | -------------------------------------------------------------------------------- /ui/app/views/modals/new_rule.html: -------------------------------------------------------------------------------- 1 | 4 | 20 | 24 | -------------------------------------------------------------------------------- /ui/app/views/modals/new_source.html: -------------------------------------------------------------------------------- 1 | 4 | 34 | 38 | -------------------------------------------------------------------------------- /ui/app/views/modals/news.html: -------------------------------------------------------------------------------- 1 | 4 | 15 | -------------------------------------------------------------------------------- /ui/app/views/modals/relationship_periods.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /ui/app/views/team/navbar.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 15 |

     

    16 |
    17 |
    18 | -------------------------------------------------------------------------------- /ui/app/views/team/permissions.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |
    5 |
    6 | Manage permissions 7 |
    8 |
    9 | 10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    Only managers can change the permissions.
    18 |
    19 |
    20 |
    21 |
    22 | 23 | 24 |
    25 |
    26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
    MemberEditorManager
    {{ user.name }}
    44 |
    45 |
    46 |
    47 | -------------------------------------------------------------------------------- /ui/app/views/team/statistics.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |
    5 | 6 |
    7 |
    8 |
    Filter by statistic
    9 |
    10 |
    11 |
    12 |
    13 |
    14 | 15 |
    16 | 19 |
    20 |
    21 |
    22 | 23 |
    24 |
    No statistic.
    25 |
    26 |
    27 |
    28 | 29 |
    30 |
    31 |
    Filter by label
    32 |
    33 |
    34 |
    35 |
    36 |
    37 | 38 |
    39 | 42 |
    43 |
    44 |
    45 | 46 |
    47 |
    No statistic.
    48 |
    49 |
    50 |
    51 | 52 |
    53 |
    -------------------------------------------------------------------------------- /ui/app/views/teams.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 5 | 6 | 9 | 10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 |

    17 | 18 | You are not a member of any team 19 |

    20 |

    You must ask a manager to join a team (you can display the managers by clicking on the user button in the bottom right of each team).

    21 |
    22 | 23 |
    24 |
    25 |

    {{ data.title }}

    26 |
    27 | 36 |
    37 |
    -------------------------------------------------------------------------------- /ui/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "depcwebui", 3 | "private": true, 4 | "version": "0.0.0", 5 | "dependencies": { 6 | "angular": "1.6.7", 7 | "bootstrap": "^3.2.0", 8 | "angular-sanitize": "1.6.7", 9 | "angular-animate": "1.6.7", 10 | "angular-cookies": "1.6.7", 11 | "angular-resource": "1.6.7", 12 | "angular-route": "1.6.7", 13 | "angular-touch": "1.6.7", 14 | "angular-filter": "^0.5.14", 15 | "angular-bootstrap": "^2.4.0", 16 | "angular-ui-codemirror": "*", 17 | "angular-schema-form": "^0.8.13", 18 | "angular-schema-form-ui-codemirror": "*", 19 | "vis": "4.21.0", 20 | "angular-visjs": "^4.16.0", 21 | "highstock": "highstock-release#^6.0.1", 22 | "highcharts-ng": "^1.1.0", 23 | "angular-toastr": "^2.1.1", 24 | "angular-confirm-modal": "^1.2.6", 25 | "angular-chart.js": "^1.1.1", 26 | "color-hash": "1.0.3", 27 | "angular-daterangepicker": "^0.2.2", 28 | "moment": "^2.10.2", 29 | "angular-bootstrap-datetimepicker": "^1.1.3", 30 | "angular-file-saver": "^1.1.3" 31 | }, 32 | "devDependencies": { 33 | "angular-mocks": "^1.4.0" 34 | }, 35 | "resolutions": { 36 | "angular": "1.6.7", 37 | "angular-cookies": "^1.4.0", 38 | "codemirror": "^5.0", 39 | "vis": "4.21.0" 40 | }, 41 | "appPath": "app", 42 | "moduleName": "depcwebuiApp", 43 | "overrides": { 44 | "bootstrap": { 45 | "main": [ 46 | "less/bootstrap.less", 47 | "dist/css/bootstrap.css", 48 | "dist/js/bootstrap.js" 49 | ] 50 | }, 51 | "codemirror": { 52 | "main": [ 53 | "lib/codemirror.js", 54 | "lib/codemirror.css", 55 | "mode/javascript/javascript.js", 56 | "theme/twilight.css" 57 | ] 58 | }, 59 | "angular-bootstrap-datetimepicker": { 60 | "main": [ 61 | "src/js/datetimepicker.js", 62 | "src/js/datetimepicker.templates.js", 63 | "src/css/datetimepicker.css" 64 | ], 65 | "dependencies": { 66 | "angular": "^1.x" 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ui/nginx/default: -------------------------------------------------------------------------------- 1 | ## 2 | # You should look at the following URL's in order to grasp a solid understanding 3 | # of Nginx configuration files in order to fully unleash the power of Nginx. 4 | # http://wiki.nginx.org/Pitfalls 5 | # http://wiki.nginx.org/QuickStart 6 | # http://wiki.nginx.org/Configuration 7 | # 8 | # Generally, you will want to move this file somewhere, and start with a clean 9 | # file but keep this around for reference. Or just disable in sites-enabled. 10 | # 11 | # Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. 12 | server { 13 | listen 80 default_server; 14 | 15 | root /var/www/html; 16 | 17 | index index.html index.htm; 18 | 19 | server_name _; 20 | 21 | location / { 22 | # First attempt to serve request as file, then 23 | # as directory, then fall back to displaying a 404. 24 | try_files $uri $uri/ =404; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "depcwebui", 3 | "private": true, 4 | "devDependencies": { 5 | "autoprefixer-core": "^5.2.1", 6 | "grunt": "^0.4.5", 7 | "grunt-angular-templates": "^0.5.7", 8 | "grunt-concurrent": "^1.0.0", 9 | "grunt-contrib-clean": "^0.6.0", 10 | "grunt-contrib-concat": "^0.5.0", 11 | "grunt-contrib-connect": "^2.0.0", 12 | "grunt-contrib-copy": "^0.7.0", 13 | "grunt-contrib-cssmin": "^0.12.0", 14 | "grunt-contrib-htmlmin": "^0.4.0", 15 | "grunt-contrib-imagemin": "^1.0.0", 16 | "grunt-contrib-jshint": "^0.11.0", 17 | "grunt-contrib-uglify": "^0.7.0", 18 | "grunt-contrib-watch": "^0.6.1", 19 | "grunt-filerev": "^2.1.2", 20 | "grunt-google-cdn": "^0.4.3", 21 | "grunt-jscs": "^1.8.0", 22 | "grunt-newer": "^1.1.0", 23 | "grunt-ng-annotate": "^0.9.2", 24 | "grunt-postcss": "^0.5.5", 25 | "grunt-string-replace": "^1.3", 26 | "grunt-svgmin": "^2.0.0", 27 | "grunt-usemin": "^3.0.0", 28 | "grunt-wiredep": "^2.0.0", 29 | "jasmine-core": "^2.5.2", 30 | "jit-grunt": "^0.9.1", 31 | "jshint-stylish": "^1.0.0", 32 | "serve-static": "^1.13.2", 33 | "time-grunt": "^1.0.0" 34 | }, 35 | "engines": { 36 | "node": ">=8.0" 37 | }, 38 | "scripts": {}, 39 | "dependencies": { 40 | "cli": ">=1.0.0", 41 | "cryptiles": ">=4.1.2", 42 | "debug": ">=2.6.9", 43 | "fresh": ">=0.5.2", 44 | "handlebars": ">=4.0.0", 45 | "hawk": ">=3.1.3", 46 | "hoek": ">=4.2.1", 47 | "lodash": ">=4.17.5", 48 | "mime": ">=1.4.1", 49 | "minimatch": ">=3.0.2", 50 | "negotiator": ">=0.6.1", 51 | "request": ">=2.68.0", 52 | "requirejs": "^2.3.6", 53 | "shell-quote": ">=1.6.1", 54 | "tar-fs": ">=1.16.2", 55 | "uglify-js": ">=2.6.0", 56 | "fstream": ">=1.0.12" 57 | } 58 | } 59 | --------------------------------------------------------------------------------