├── .coveragerc ├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── chaosplatform-sample.toml ├── chaosplatform ├── __init__.py ├── __main__.py ├── api.py ├── app.py ├── auth.py ├── cache.py ├── cli.py ├── log.py ├── logging.json ├── server.py ├── service │ └── __init__.py ├── settings.py ├── storage.py └── views │ └── __init__.py ├── ci.bash ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── conftest.py ├── fixtures ├── dummy-logging.json └── testconfig.toml └── test_log.py /.coveragerc: -------------------------------------------------------------------------------- 1 | 2 | [report] 3 | show_missing = true 4 | precision = 2 5 | omit = *migrations* 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 11 | 12 | [*.{bat,cmd,ps1}] 13 | end_of_line = crlf 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | .eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | wheelhouse 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | venv*/ 23 | pyvenv*/ 24 | 25 | # Installer logs 26 | pip-log.txt 27 | 28 | # Unit test / coverage reports 29 | .coverage 30 | .tox 31 | .coverage.* 32 | .pytest_cache/ 33 | nosetests.xml 34 | coverage.xml 35 | htmlcov 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | .idea 45 | *.iml 46 | *.komodoproject 47 | 48 | # Complexity 49 | output/*.html 50 | output/*/index.html 51 | 52 | # Sphinx 53 | docs/_build 54 | 55 | .DS_Store 56 | *~ 57 | .*.sw[po] 58 | .build 59 | .ve 60 | .env 61 | .cache 62 | .pytest 63 | .bootstrap 64 | .appveyor.token 65 | *.bak 66 | 67 | # Mypy Cache 68 | .mypy_cache/ 69 | 70 | .pytest_cache 71 | .coverage 72 | coverage.xml 73 | test-results.xml 74 | .vscode/ 75 | errors.log 76 | access.log 77 | 78 | !tests/fixtures/.env 79 | chaosplatform.toml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | cache: pip 4 | env: 5 | global: 6 | - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 7 | - SEGFAULT_SIGNALS=all 8 | - PYPI_USER_NAME: chaostoolkit 9 | - secure: "apf1ro4EZhRpiW1OHJ6lFm9HCtQjMOsrhrmVivN+XvPJjBa+mYTZNFaS+K4s0VFSMn/MzGWZEf9nPIlDRyx9L97wW/nk6d1slDXRGIvxFEnkd3eDONTk0n7DcokbsauKizsscniU6uuvjZmLxyEFTQTkYnxmMUPhY8gsvsVTkmDG2TPkZqaTDVpSkzpRyxiv0cNhkn9f0U2StE5alxFkkErTtINLvENyZvGVKKHtuLorv32U5rtoVBr7JhruQ53NhVsqwDlnCUgREx+6qyf8pNqDy8aXHkTwpQenSxycBY2KEjLdf2DTQmNziex9tndDxuFlR5qaGggA1In53rlyCcH5JQBRWgkjuHLE5sLKpCBg1iYeG44P+TIxHj2r5Rpmm2AjUZsD/Fa+iu8akJAi/ZYIdqbwInna3H0Hgns6xpJuFg8XQR96kjsTc4E7qxepj2NgkRvFZRGJPayCbuZqfp88BCG3Bu7sm4bllylIgMT4viWXBtE4dblqz0AmTKN5v3EMlIeqO/GD/YnQb9H7OkApvbtpBiLcX5l0IwOqHRCYiiUqknGaAZGCKcLQyvaKUB9lJRRVRpJKvDayYUjcdPtiBs3doazbq6EBz1qsVvssl/5Uv2RreVEt+ag9EEIm53jPqg6FBZsUHp0Rhy1L9rEDIhNHE8VwuTH+HGn7yoM=" 10 | python: 11 | - "3.6" 12 | - "3.7-dev" 13 | install: 14 | - virtualenv --version 15 | - pip install -U pip setuptools 16 | - pip --version 17 | - pip install --pre -r requirements.txt -r requirements-dev.txt 18 | script: 19 | - bash ci.bash 20 | notifications: 21 | webhooks: 22 | urls: 23 | - "https://webhook.atomist.com/atomist/travis/teams/T76U4GPGF" 24 | on_cancel: always 25 | on_error: always 26 | on_start: always 27 | on_failure: always 28 | on_success: always 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased][] 4 | 5 | [Unreleased]: https://github.com/chaostoolkit/chaosplatform/compare/0.2.0...HEAD 6 | 7 | ## [0.2.0][] - 2019-01-14 8 | 9 | [0.2.0]: https://github.com/chaostoolkit/chaosplatform/compare/0.1.2...0.2.0 10 | 11 | ### Changed 12 | 13 | - Ensure all services can consume the new toml configuration 14 | 15 | ## [0.1.2][] - 2019-01-14 16 | 17 | [0.1.2]: https://github.com/chaostoolkit/chaosplatform/compare/0.1.1...0.1.2 18 | 19 | ### Changed 20 | 21 | - Ensure recent relational db package so its dependencies are installed too 22 | 23 | ## [0.1.1][] - 2019-01-12 24 | 25 | [0.1.1]: https://github.com/chaostoolkit/chaosplatform/compare/0.1.0...0.1.1 26 | 27 | ### Changed 28 | 29 | - Fix dependencies 30 | 31 | ## [0.1.0][] - 2019-01-12 32 | 33 | [0.1.0]: https://github.com/chaostoolkit/chaosplatform/tree/0.1.0 34 | 35 | ### Added 36 | 37 | - Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft tests 2 | 3 | include .coveragerc 4 | include .editorconfig 5 | 6 | include CHANGELOG.md 7 | include LICENSE 8 | include README.md 9 | include requirements.txt 10 | include requirements-dev.txt 11 | include chaosplatform/logging.json 12 | 13 | global-exclude *.py[cod] __pycache__ 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chaos Platform - Chaos Engineering Platform for Everyone 2 | 3 | [![Version](https://img.shields.io/pypi/v/chaosplatform.svg)](https://img.shields.io/pypi/v/chaosplatform.svg) 4 | [![License](https://img.shields.io/pypi/l/chaosplatform.svg)](https://img.shields.io/pypi/l/chaosplatform.svg) 5 | [![StackOverflow](https://img.shields.io/badge/StackOverflow-ChaosPlatform-blue.svg)](https://stackoverflow.com/questions/tagged/chaosplatform+or+chaostoolkit) 6 | 7 | [![Build Status](https://travis-ci.org/chaostoolkit/chaosplatform.svg?branch=master)](https://travis-ci.org/chaostoolkit/chaosplatform) 8 | [![Python versions](https://img.shields.io/pypi/pyversions/chaosplatform.svg)](https://www.python.org/) 9 | 10 | This is the [Chaos Platform][chaosplatform] main project. 11 | 12 | [chaosplatform]: https://chaosplatform.org/ 13 | 14 | * WARNING*: This is an alpha release so expect things to get rocky and break for 15 | now. We are heavily working on it although the API should remain stable. Please, 16 | join the [slack][slack] to keep the discussion alive :) Thank you for being 17 | patient with us! 18 | 19 | [slack]: https://join.chaostoolkit.org/ 20 | 21 | ## Install & Run 22 | 23 | Documentation is being written so the instructions here are for the courageous. 24 | 25 | * Install Python 3.6+. No promises are made for lower versions. 26 | * Create a Python virtual environment 27 | * Install pip 28 | * Install Redis via a simple Docker image 29 | 30 | ``` 31 | $ docker run --rm --name redis -p 6379:6379 redis 32 | ``` 33 | 34 | * Install the Chaos Platform 35 | 36 | ``` 37 | $ pip install --pre -U chaosplatform 38 | ``` 39 | 40 | * Create a chaosplatform.toml configuration file: 41 | 42 | 43 | ```toml 44 | [chaosplatform] 45 | debug = false 46 | 47 | [chaosplatform.grpc] 48 | address = "0.0.0.0:50051" 49 | 50 | [chaosplatform.http] 51 | address = "0.0.0.0:8090" 52 | secret_key = "" 53 | 54 | [chaosplatform.http.cherrypy] 55 | environment = "production" 56 | proxy = "http://localhost:6080" 57 | 58 | [chaosplatform.cache] 59 | type = "simple" 60 | 61 | # Only set if type is set to "redis" 62 | # [chaosplatform.cache.redis] 63 | # host = "localhost" 64 | # port = 6379 65 | 66 | [chaosplatform.db] 67 | uri = "sqlite:///:memory" 68 | 69 | [chaosplatform.jwt] 70 | secret_key = "" 71 | public_key = "" 72 | algorithm = "HS256" 73 | identity_claim_key = "identity" 74 | user_claims_key = "user_claims" 75 | access_token_expires = 2592000 76 | refresh_token_expires = 31536000 77 | user_claims_in_refresh_token = false 78 | 79 | [chaosplatform.account] 80 | 81 | [chaosplatform.auth] 82 | [chaosplatform.auth.oauth2] 83 | [chaosplatform.auth.oauth2.github] 84 | client_id = "" 85 | client_secret = "" 86 | 87 | [chaosplatform.auth.grpc] 88 | [chaosplatform.auth.grpc.account] 89 | address = "0.0.0.0:50051" 90 | 91 | [chaosplatform.experiment] 92 | 93 | [chaosplatform.scheduling] 94 | [chaosplatform.scheduling.grpc] 95 | [chaosplatform.scheduling.grpc.scheduler] 96 | address = "0.0.0.0:50051" 97 | 98 | [chaosplatform.scheduler] 99 | [chaosplatform.scheduler.redis] 100 | host = "localhost" 101 | port = 6379 102 | queue = "chaosplatform" 103 | 104 | [chaosplatform.scheduler.job] 105 | platform_url = "http://127.0.0.1:6080" 106 | 107 | [chaosplatform.scheduler.worker] 108 | debug = false 109 | count = 3 110 | queue_name = "chaosplatform" 111 | worker_name = "chaosplatform-worker" 112 | add_random_suffix_to_worker_name = true 113 | worker_directory = "/tmp" 114 | 115 | [chaosplatform.scheduler.worker.redis] 116 | host = "localhost" 117 | port = 6379 118 | ``` 119 | 120 | * Run the Chaos Platform: 121 | 122 | ``` 123 | $ chaosplatform run --config=chaosplatform.toml 124 | ``` 125 | 126 | For now, the platform is GUI-less so needs to be called bia its [API][openapi]. 127 | 128 | [openapi]: https://github.com/chaostoolkit/chaosplatform-openapi 129 | 130 | ## Contribute 131 | 132 | Contributors to this project are welcome as this is an open-source effort that 133 | seeks [discussions][join] and continuous improvement. 134 | 135 | [join]: https://join.chaostoolkit.org/ 136 | 137 | From a code perspective, if you wish to contribute, you will need to run a 138 | Python 3.5+ environment. Then, fork this repository and submit a PR. The 139 | project cares for code readability and checks the code style to match best 140 | practices defined in [PEP8][pep8]. Please also make sure you provide tests 141 | whenever you submit a PR so we keep the code reliable. 142 | 143 | [pep8]: https://pycodestyle.readthedocs.io/en/latest/ 144 | 145 | The Chaos Platform projects require all contributors must sign a 146 | [Developer Certificate of Origin][dco] on each commit they would like to merge 147 | into the master branch of the repository. Please, make sure you can abide by 148 | the rules of the DCO before submitting a PR. 149 | 150 | [dco]: https://github.com/probot/dco#how-it-works -------------------------------------------------------------------------------- /chaosplatform-sample.toml: -------------------------------------------------------------------------------- 1 | [chaosplatform] 2 | debug = false 3 | 4 | [chaosplatform.grpc] 5 | address = "0.0.0.0:50051" 6 | 7 | [chaosplatform.http] 8 | address = "0.0.0.0:8090" 9 | secret_key = "" 10 | 11 | [chaosplatform.http.cherrypy] 12 | environment = "production" 13 | proxy = "http://localhost:6080" 14 | 15 | [chaosplatform.cache] 16 | type = "simple" 17 | 18 | # Only set if type is set to "redis" 19 | # [chaosplatform.cache.redis] 20 | # host = "localhost" 21 | # port = 6379 22 | 23 | [chaosplatform.db] 24 | uri = "sqlite:///:memory" 25 | 26 | [chaosplatform.jwt] 27 | secret_key = "" 28 | public_key = "" 29 | algorithm = "HS256" 30 | identity_claim_key = "identity" 31 | user_claims_key = "user_claims" 32 | access_token_expires = 2592000 33 | refresh_token_expires = 31536000 34 | user_claims_in_refresh_token = false 35 | 36 | [chaosplatform.account] 37 | 38 | [chaosplatform.auth] 39 | [chaosplatform.auth.oauth2] 40 | [chaosplatform.auth.oauth2.github] 41 | client_id = "" 42 | client_secret = "" 43 | 44 | [chaosplatform.auth.grpc] 45 | [chaosplatform.auth.grpc.account] 46 | address = "0.0.0.0:50051" 47 | 48 | [chaosplatform.experiment] 49 | 50 | [chaosplatform.scheduling] 51 | [chaosplatform.scheduling.grpc] 52 | [chaosplatform.scheduling.grpc.scheduler] 53 | address = "0.0.0.0:50051" 54 | 55 | [chaosplatform.scheduler] 56 | [chaosplatform.scheduler.redis] 57 | host = "localhost" 58 | port = 6379 59 | queue = "chaosplatform" 60 | 61 | [chaosplatform.scheduler.job] 62 | platform_url = "http://127.0.0.1:6080" 63 | 64 | [chaosplatform.scheduler.worker] 65 | debug = false 66 | count = 3 67 | queue_name = "chaosplatform" 68 | worker_name = "chaosplatform-worker" 69 | add_random_suffix_to_worker_name = true 70 | worker_directory = "/tmp" 71 | 72 | [chaosplatform.scheduler.worker.redis] 73 | host = "localhost" 74 | port = 6379 -------------------------------------------------------------------------------- /chaosplatform/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2.0' 2 | -------------------------------------------------------------------------------- /chaosplatform/__main__.py: -------------------------------------------------------------------------------- 1 | from chaosplatform.cli import cli 2 | 3 | 4 | if __name__ == "__main__": 5 | cli() 6 | -------------------------------------------------------------------------------- /chaosplatform/api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging import StreamHandler 3 | from typing import Any, Dict 4 | 5 | import cherrypy 6 | from flask import Flask 7 | from requestlogger import ApacheFormatter, WSGILogger 8 | from werkzeug.contrib.fixers import ProxyFix 9 | 10 | from .auth import setup_jwt, setup_login 11 | 12 | __all__ = ["cleanup_api", "create_api", "serve_api"] 13 | logger = logging.getLogger("chaosplatform") 14 | 15 | 16 | def create_api(config: Dict[str, Any]) -> Flask: 17 | """ 18 | Create the Flask application and initialize its resources. 19 | """ 20 | app = Flask(__name__) 21 | 22 | app.url_map.strict_slashes = False 23 | app.debug = config.get("debug", False) 24 | 25 | logger = logging.getLogger('flask.app') 26 | logger.propagate = False 27 | 28 | app.config["SECRET_KEY"] = config["http"]["secret_key"] 29 | app.secret_key = config["http"]["secret_key"] 30 | app.config["JWT_SECRET_KEY"] = config["jwt"]["secret_key"] 31 | app.config['JWT_BLACKLIST_ENABLED'] = True 32 | app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['access', 'refresh'] 33 | app.config["SQLALCHEMY_DATABASE_URI"] = config["db"]["uri"] 34 | 35 | app.config["CACHE_TYPE"] = config["cache"].get("type", "simple") 36 | if app.config["CACHE_TYPE"] == "redis": 37 | redis_config = config["cache"]["redis"] 38 | app.config["CACHE_REDIS_HOST"] = redis_config.get("host") 39 | app.config["CACHE_REDIS_PORT"] = redis_config.get("port", 6379) 40 | app.config["CACHE_REDIS_DB"] = redis_config.get("db", 0) 41 | app.config["CACHE_REDIS_PASSWORD"] = redis_config.get("password") 42 | 43 | setup_jwt(app) 44 | setup_login(app, from_session=True, from_jwt=True) 45 | 46 | return app 47 | 48 | 49 | def cleanup_api(settings: Dict[str, Any]): 50 | pass 51 | 52 | 53 | def serve_api(app: Flask, mount_point: str = '/', 54 | log_handler: StreamHandler = None): 55 | log_handler = log_handler or logging.StreamHandler() 56 | app.wsgi_app = ProxyFix(app.wsgi_app) 57 | wsgiapp = WSGILogger( 58 | app.wsgi_app, [log_handler], ApacheFormatter(), 59 | propagate=False) 60 | cherrypy.tree.graft(wsgiapp, mount_point) 61 | -------------------------------------------------------------------------------- /chaosplatform/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging import StreamHandler 3 | from typing import Any, Dict 4 | 5 | import cherrypy 6 | from flask import Flask 7 | from requestlogger import ApacheFormatter, WSGILogger 8 | from werkzeug.contrib.fixers import ProxyFix 9 | 10 | from .auth import setup_login 11 | 12 | __all__ = ["cleanup_app", "create_app", "serve_app"] 13 | logger = logging.getLogger("chaosplatform") 14 | 15 | 16 | def create_app(config: Dict[str, Any]) -> Flask: 17 | """ 18 | Create the Flask application. 19 | """ 20 | app = Flask(__name__) 21 | app.url_map.strict_slashes = False 22 | app.debug = config.get("debug", False) 23 | 24 | logger = logging.getLogger('flask.app') 25 | logger.propagate = False 26 | 27 | app.config["SECRET_KEY"] = config["http"]["secret_key"] 28 | app.secret_key = config["http"]["secret_key"] 29 | app.config["JWT_SECRET_KEY"] = config["jwt"]["secret_key"] 30 | app.config["SQLALCHEMY_DATABASE_URI"] = config["db"]["uri"] 31 | 32 | app.config["CACHE_TYPE"] = config["cache"].get("type", "simple") 33 | if app.config["CACHE_TYPE"] == "redis": 34 | redis_config = config["cache"]["redis"] 35 | app.config["CACHE_REDIS_HOST"] = redis_config.get("host") 36 | app.config["CACHE_REDIS_PORT"] = redis_config.get("port", 6379) 37 | app.config["CACHE_REDIS_DB"] = redis_config.get("db", 0) 38 | app.config["CACHE_REDIS_PASSWORD"] = redis_config.get("password") 39 | 40 | # OAUTH2 41 | oauth2_config = config["auth"]["oauth2"] 42 | for backend in oauth2_config: 43 | provider = backend.upper() 44 | provider_config = oauth2_config[backend] 45 | 46 | app.config["{}_OAUTH_CLIENT_ID".format(provider)] = \ 47 | provider_config["client_id"] 48 | app.config["{}_OAUTH_CLIENT_SECRET".format(provider)] = \ 49 | provider_config["client_secret"] 50 | 51 | setup_login(app, from_session=True) 52 | 53 | return app 54 | 55 | 56 | def cleanup_app(settings: Dict[str, Any]): 57 | pass 58 | 59 | 60 | def serve_app(app: Flask, mount_point: str = '/', 61 | log_handler: StreamHandler = None): 62 | log_handler = log_handler or logging.StreamHandler() 63 | app.wsgi_app = ProxyFix(app.wsgi_app) 64 | wsgiapp = WSGILogger( 65 | app.wsgi_app, [log_handler], ApacheFormatter(), 66 | propagate=False) 67 | cherrypy.tree.graft(wsgiapp, mount_point) 68 | -------------------------------------------------------------------------------- /chaosplatform/auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import Any, Dict, Union 3 | from uuid import UUID 4 | 5 | from chaosplt_account.model import User 6 | from flask import Flask, request, Request 7 | from flask_login import LoginManager 8 | from flask_jwt_extended import JWTManager, verify_jwt_in_request, \ 9 | current_user as api_user, get_jti 10 | 11 | __all__ = ["setup_jwt", "setup_login"] 12 | 13 | 14 | def setup_jwt(app: Flask) -> JWTManager: 15 | jwt = JWTManager(app) 16 | 17 | @jwt.user_claims_loader 18 | def add_claims(identity: Union[UUID, str]) -> Dict[str, Any]: 19 | return { 20 | 'user_id': str(identity) 21 | } 22 | 23 | @jwt.user_loader_callback_loader 24 | def user_loader(identity: Union[UUID, str]) -> User: 25 | return request.services.account.registration.get(identity) 26 | 27 | @jwt.token_in_blacklist_loader 28 | def check_if_token_in_blacklist(decrypted_token: Dict[str, Any]) -> bool: 29 | """ 30 | Check that the token is active 31 | 32 | When the token has been revoked or has been deleted, this returns 33 | `True` to indicate that the token should not be allowed. 34 | """ 35 | user_id = decrypted_token["identity"] 36 | jti = decrypted_token["jti"] 37 | token = request.services.auth.access_token.get_by_jti(user_id, jti) 38 | if not token or token.revoked: 39 | return True 40 | return False 41 | 42 | return jwt 43 | 44 | 45 | def setup_login(app: Flask, from_session: bool = False, 46 | from_jwt: bool = False) -> LoginManager: 47 | login_manager = LoginManager() 48 | login_manager.init_app(app) 49 | login_manager.login_view = 'github.login' 50 | 51 | if from_session: 52 | @login_manager.user_loader 53 | def load_user_from_session(user_id: Union[UUID, str]) -> User: 54 | return request.services.account.registration.get(user_id) 55 | 56 | if from_jwt: 57 | @login_manager.request_loader 58 | def load_user_from_request(request: Request): 59 | verify_jwt_in_request() 60 | return api_user 61 | 62 | return login_manager 63 | -------------------------------------------------------------------------------- /chaosplatform/cache.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_caching import Cache 3 | 4 | __all__ = ["setup_cache"] 5 | 6 | 7 | def setup_cache(app: Flask) -> Cache: 8 | """ 9 | Initialize the application's cache. 10 | """ 11 | return Cache(app, config=app.config) 12 | -------------------------------------------------------------------------------- /chaosplatform/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from . import __version__ 4 | from .log import configure_logger 5 | from .server import run_forever 6 | from .settings import load_settings 7 | 8 | 9 | @click.group() 10 | @click.version_option(version=__version__) 11 | def cli(): 12 | pass 13 | 14 | 15 | @cli.command() 16 | @click.option('--config', 17 | type=click.Path(exists=True, readable=True, resolve_path=True), 18 | help='Configuration TOML file.') 19 | @click.option('--logger-config', 20 | type=click.Path(exists=False, readable=True, resolve_path=True), 21 | help='Python logger JSON definition.') 22 | @click.option('--with-ui', is_flag=True, default=False, show_default=True, 23 | help='Run the UI') 24 | def run(config: str = None, logger_config: str = None, with_ui: bool = False): 25 | """ 26 | Runs the application. 27 | """ 28 | config = load_settings(config) 29 | config["ui"] = with_ui 30 | configure_logger(logger_config, config) 31 | run_forever(config) 32 | -------------------------------------------------------------------------------- /chaosplatform/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.config 3 | from logging import StreamHandler 4 | import pkgutil 5 | from typing import Any, Dict 6 | 7 | from flask import Flask 8 | from requestlogger import ApacheFormatter, WSGILogger 9 | import simplejson as json 10 | 11 | __all__ = ["clean_logger", "configure_logger", "http_requests_logger"] 12 | 13 | logger = logging.getLogger("chaosplatform") 14 | 15 | 16 | def configure_logger(log_conf_path: str, config: Dict[str, Any]): 17 | if log_conf_path: 18 | with open(log_conf_path) as f: 19 | log_conf = json.load(f) 20 | else: 21 | log_conf = json.loads( 22 | pkgutil.get_data('chaosplatform', 'logging.json')) 23 | logging.config.dictConfig(log_conf) 24 | if config["debug"]: 25 | [h.setLevel(logging.DEBUG) for h in logger.handlers] 26 | logger.setLevel(logging.DEBUG) 27 | logger.debug("Logger configured") 28 | 29 | 30 | def clean_logger(): 31 | """ 32 | Clean all handlers attached to the logger 33 | """ 34 | for h in logger.handlers[:]: 35 | logger.removeHandler(h) 36 | 37 | 38 | def http_requests_logger(app: Flask, 39 | stream_handler: StreamHandler = None) -> WSGILogger: 40 | if not stream_handler: 41 | stream_handler = StreamHandler() 42 | return WSGILogger( 43 | app.wsgi_app, [stream_handler], ApacheFormatter(), propagate=False) 44 | -------------------------------------------------------------------------------- /chaosplatform/logging.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | 4 | "formatters": { 5 | "void": { 6 | "format": "" 7 | }, 8 | "standard": { 9 | "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s" 10 | } 11 | }, 12 | "handlers": { 13 | "default": { 14 | "level":"INFO", 15 | "class":"logging.StreamHandler", 16 | "formatter": "standard", 17 | "stream": "ext://sys.stdout" 18 | }, 19 | "cherrypy_console": { 20 | "level":"INFO", 21 | "class":"logging.StreamHandler", 22 | "formatter": "void", 23 | "stream": "ext://sys.stdout" 24 | }, 25 | "cherrypy_access": { 26 | "level":"INFO", 27 | "class": "logging.handlers.RotatingFileHandler", 28 | "formatter": "void", 29 | "filename": "access.log", 30 | "maxBytes": 10485760, 31 | "backupCount": 20, 32 | "encoding": "utf8" 33 | }, 34 | "cherrypy_access_stdout": { 35 | "level":"INFO", 36 | "class": "logging.StreamHandler", 37 | "formatter": "void", 38 | "stream": "ext://sys.stdout" 39 | }, 40 | "cherrypy_error": { 41 | "level":"INFO", 42 | "class": "logging.handlers.RotatingFileHandler", 43 | "formatter": "void", 44 | "filename": "errors.log", 45 | "maxBytes": 10485760, 46 | "backupCount": 20, 47 | "encoding": "utf8" 48 | } 49 | }, 50 | "loggers": { 51 | "": { 52 | "handlers": ["default"], 53 | "level": "INFO", 54 | "propagate": false 55 | }, 56 | "chaosplatform": { 57 | "handlers": ["default"], 58 | "level": "INFO" , 59 | "propagate": false 60 | }, 61 | "rq.worker": { 62 | "handlers": ["default"], 63 | "level": "INFO" , 64 | "propagate": false 65 | }, 66 | "cherrypy.access": { 67 | "handlers": ["cherrypy_access", "cherrypy_access_stdout"], 68 | "level": "INFO", 69 | "propagate": false 70 | }, 71 | "cherrypy.error": { 72 | "handlers": ["cherrypy_console", "cherrypy_error"], 73 | "level": "INFO", 74 | "propagate": false 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /chaosplatform/server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from typing import Any, Dict, Tuple 4 | 5 | from chaosplt_account.server import initialize_all as init_account, \ 6 | release_all as release_account 7 | from chaosplt_auth.server import initialize_all as init_auth, \ 8 | release_all as release_auth 9 | from chaosplt_experiment.server import initialize_all as init_experiment, \ 10 | release_all as release_experiment 11 | from chaosplt_scheduler.server import initialize_all as init_scheduler, \ 12 | release_all as release_scheduler 13 | from chaosplt_scheduling.server import initialize_all as init_scheduling, \ 14 | release_all as release_scheduling 15 | from chaosplt_grpc import create_grpc_server, start_grpc_server, \ 16 | stop_grpc_server 17 | from chaosplt_dashboard.server import serve_ui 18 | import cherrypy 19 | from flask import Flask 20 | from grpc import Server as GRPCServer 21 | 22 | from .app import create_app, cleanup_app, serve_app 23 | from .api import create_api, cleanup_api, serve_api 24 | from .cache import setup_cache 25 | from .service import Services 26 | 27 | __all__ = ["initialize_all", "release_all", "run_forever"] 28 | logger = logging.getLogger("chaosplatform") 29 | 30 | 31 | def initialize_all(config: Dict[str, Any]) \ 32 | -> Tuple[Flask, Flask, Services, GRPCServer, Tuple, Tuple, 33 | Tuple, Tuple, Tuple]: 34 | services = Services() 35 | 36 | access_log_handler = logging.StreamHandler() 37 | 38 | web_app = create_app(config) 39 | web_cache = setup_cache(web_app) 40 | 41 | api_app = create_api(config) 42 | api_cache = setup_cache(api_app) 43 | 44 | grpc_server = create_grpc_server(config["grpc"]["address"]) 45 | start_grpc_server(grpc_server) 46 | 47 | account_resources = init_account( 48 | config["account"], web_app, api_app, services, grpc_server, web_cache, 49 | api_cache, 50 | web_mount_point="/account", api_mount_point="/account", 51 | access_log_handler=access_log_handler) 52 | auth_resources = init_auth( 53 | config["auth"], web_app, api_app, services, grpc_server, web_cache, 54 | api_cache, 55 | web_mount_point="/auth", api_mount_point="/auth", 56 | access_log_handler=access_log_handler) 57 | scheduler_resources = init_scheduler( 58 | config["scheduler"], services, grpc_server) 59 | scheduling_resources = init_scheduling( 60 | config["scheduling"], web_app, api_app, services, grpc_server, 61 | web_cache, api_cache, 62 | web_mount_point="/scheduling", api_mount_point="/scheduling", 63 | access_log_handler=access_log_handler) 64 | experiment_resources = init_experiment( 65 | config["experiment"], web_app, api_app, services, grpc_server, 66 | web_cache, api_cache, 67 | experiment_web_mount_point="/experi ment", 68 | experiment_api_mount_point="/experiments", 69 | execution_web_mount_point="/execution", 70 | execution_api_mount_point="/executions", 71 | access_log_handler=access_log_handler) 72 | 73 | serve_app(web_app, "/", access_log_handler) 74 | serve_api(api_app, "/api/v1", access_log_handler) 75 | 76 | if config.get("ui") is True: 77 | serve_ui() 78 | 79 | return ( 80 | web_app, api_app, services, grpc_server, auth_resources, 81 | account_resources, scheduler_resources, scheduling_resources, 82 | experiment_resources) 83 | 84 | 85 | def release_all(services: Services, web_app: Flask, api_app: Flask, 86 | grpc_server: GRPCServer, 87 | auth_resources: Tuple, account_storage: Tuple, 88 | scheduler_resources: Tuple, scheduling_resources: Tuple, 89 | experiment_resources: Tuple): 90 | stop_grpc_server(grpc_server) 91 | cleanup_app(web_app) 92 | cleanup_api(api_app) 93 | release_scheduler(*scheduler_resources) 94 | release_account(*account_storage) 95 | release_auth(*auth_resources) 96 | release_scheduling(*scheduling_resources) 97 | release_experiment(*experiment_resources) 98 | 99 | 100 | def run_forever(config: Dict[str, Any]): 101 | """ 102 | Run and block until a signal is sent to the process. 103 | 104 | The application, services or gRPC server are all created and initialized 105 | when the application starts. 106 | """ 107 | def run_stuff(config: Dict[str, Any]): 108 | resources = initialize_all(config) 109 | cherrypy.engine.subscribe( 110 | 'stop', lambda: release_all(*resources), 111 | priority=20) 112 | 113 | cherrypy.engine.subscribe( 114 | 'start', lambda: run_stuff(config), priority=80) 115 | 116 | if "tls" in config["http"]: 117 | cherrypy.server.ssl_module = 'builtin' 118 | cherrypy.server.ssl_certificate = config["http"]["tls"]["certificate"] 119 | cherrypy.server.ssl_private_key = config["http"]["tls"]["key"] 120 | 121 | cherrypy.engine.signals.subscribe() 122 | cherrypy.engine.start() 123 | cherrypy.engine.block() 124 | -------------------------------------------------------------------------------- /chaosplatform/service/__init__.py: -------------------------------------------------------------------------------- 1 | import attr 2 | 3 | __all__ = ["Services"] 4 | 5 | 6 | @attr.s 7 | class Services: 8 | auth: object = attr.ib(default=None) 9 | account: object = attr.ib(default=None) 10 | experiment: object = attr.ib(default=None) 11 | execution: object = attr.ib(default=None) 12 | scheduling: object = attr.ib(default=None) 13 | scheduler: object = attr.ib(default=None) 14 | worker: object = attr.ib(default=None) 15 | -------------------------------------------------------------------------------- /chaosplatform/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from copy import deepcopy 3 | from typing import Any, Dict 4 | 5 | import cherrypy 6 | import toml 7 | 8 | __all__ = ["load_settings"] 9 | 10 | 11 | def load_settings(toml_path: str) -> Dict[str, Any]: 12 | """ 13 | """ 14 | config = toml.load(toml_path) 15 | populate_config_from_root(config) 16 | 17 | debug = config["chaosplatform"]["debug"] 18 | server_addr = config["chaosplatform"]["http"]["address"] 19 | host, port = server_addr.rsplit(":", 1) 20 | default_cherrypy_env = "" if debug else "production" 21 | 22 | cherrypy_config = config["chaosplatform"]["http"]["cherrypy"] 23 | cherrypy.engine.unsubscribe('graceful', cherrypy.log.reopen_files) 24 | cherrypy.config.update({ 25 | 'server.socket_host': host, 26 | 'server.socket_port': int(port), 27 | 'engine.autoreload.on': False, 28 | 'checker.on': False, 29 | 'log.screen': debug, 30 | 'log.access_file': cherrypy_config.get("access_file", ""), 31 | 'log.error_file': cherrypy_config.get("error_file", ""), 32 | 'environment': cherrypy_config.get( 33 | "environment", default_cherrypy_env) 34 | }) 35 | 36 | if "proxy" in config["chaosplatform"]["http"]: 37 | cherrypy.config.update({ 38 | 'tools.proxy.on': True, 39 | 'tools.proxy.base': config["chaosplatform"]["http"]["proxy"] 40 | }) 41 | 42 | return config["chaosplatform"] 43 | 44 | 45 | def populate_config_from_root(config: Dict[str, Any]): 46 | """ 47 | Populate all global configuration settings down to each service 48 | when they aren't set yet. 49 | """ 50 | root_config = config.get("chaosplatform") 51 | debug = root_config.get("debug", False) 52 | 53 | root_config["http"].setdefault("debug", debug) 54 | root_config["db"].setdefault("debug", debug) 55 | root_config["grpc"].setdefault("debug", debug) 56 | 57 | for svc in ('account', 'auth', 'experiment', 'scheduling', 'scheduler'): 58 | svc_config = root_config.get(svc) 59 | if not svc_config: 60 | svc_config = {} 61 | root_config[svc] = svc_config 62 | 63 | svc_config.setdefault("debug", debug) 64 | 65 | jwt_config = deepcopy(root_config['jwt']) 66 | svc_config['jwt'] = jwt_config 67 | 68 | cache_config = deepcopy(root_config['cache']) 69 | svc_config['cache'] = cache_config 70 | 71 | http_config = root_config.get("http") 72 | if "http" not in svc_config: 73 | svc_config["http"] = {} 74 | 75 | svc_config["http"].setdefault("debug", debug) 76 | svc_config["http"].setdefault("address", http_config.get("address")) 77 | svc_config["http"].setdefault("proxy", http_config.get("proxy")) 78 | svc_config["http"].setdefault( 79 | "secret_key", http_config.get("secret_key")) 80 | svc_config["http"].setdefault( 81 | "environment", http_config.get("environment", "production")) 82 | 83 | db_config = root_config.get("db") 84 | if "db" not in svc_config: 85 | svc_config["db"] = {} 86 | 87 | svc_config["db"].setdefault("debug", debug) 88 | svc_config["db"].setdefault("uri", db_config.get("uri")) 89 | 90 | grpc_config = root_config.get("grpc") 91 | if "grpc" not in svc_config: 92 | svc_config["grpc"] = {} 93 | 94 | svc_config["grpc"].setdefault("debug", debug) 95 | svc_config["grpc"].setdefault("address", grpc_config.get("address")) 96 | -------------------------------------------------------------------------------- /chaosplatform/storage.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | try: 4 | from chaosplt_relational_storage import initialize_storage 5 | from chaosplt_relational_storage import configure_storage 6 | from chaosplt_relational_storage import release_storage 7 | HAS_RELATIONAL_STORAGE_PACKAGE = True 8 | except ImportError: 9 | HAS_RELATIONAL_STORAGE_PACKAGE = False 10 | 11 | __all__ = ["get_storage_driver", "configure_storage_driver", 12 | "release_storage_driver"] 13 | 14 | 15 | def get_storage_driver(provider: str, config: Dict[str, Any]) -> object: 16 | if provider == "native": 17 | if not HAS_RELATIONAL_STORAGE_PACKAGE: 18 | raise RuntimeError( 19 | "'chaosplatform-relational-storage' not installed") 20 | return initialize_storage(config) 21 | 22 | 23 | def configure_storage_driver(provider: str, storage_driver: object): 24 | if provider == "native": 25 | if not HAS_RELATIONAL_STORAGE_PACKAGE: 26 | raise RuntimeError( 27 | "'chaosplatform-relational-storage' not installed") 28 | configure_storage(storage_driver) 29 | 30 | 31 | def release_storage_driver(provider: str, storage_driver: object): 32 | if provider == "native": 33 | if not HAS_RELATIONAL_STORAGE_PACKAGE: 34 | raise RuntimeError( 35 | "'chaosplatform-relational-storage' not installed") 36 | return release_storage(storage_driver) 37 | -------------------------------------------------------------------------------- /chaosplatform/views/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # -*- coding: utf-8 -*- 3 | from flask import Blueprint, Flask 4 | from flask_login import login_required 5 | 6 | __all__ = ["register_views"] 7 | 8 | plane = Blueprint("control_plane", __name__) 9 | 10 | 11 | def register_views(app: Flask, prefix: str = '/'): 12 | prefix = prefix.rstrip("/") 13 | app.register_blueprint(plane, url_prefix="{}/".format(prefix)) 14 | 15 | 16 | @plane.route('') 17 | @login_required 18 | def index(): 19 | return "hello" 20 | -------------------------------------------------------------------------------- /ci.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | function lint () { 5 | echo "Checking the code syntax" 6 | pylama chaosplatform 7 | } 8 | 9 | function build () { 10 | echo "Building the package" 11 | python setup.py build 12 | } 13 | 14 | function run-test () { 15 | echo "Running the tests" 16 | python setup.py test 17 | } 18 | 19 | function release () { 20 | echo "Releasing the package" 21 | python setup.py sdist bdist_wheel 22 | 23 | echo "Publishing to PyPI" 24 | pip install twine 25 | twine upload dist/* -u ${PYPI_USER_NAME} -p ${PYPI_PWD} 26 | } 27 | 28 | function main () { 29 | lint || return 1 30 | build || return 1 31 | run-test || return 1 32 | 33 | if [[ $TRAVIS_PYTHON_VERSION =~ ^3\.6+$ ]]; then 34 | if [[ $TRAVIS_TAG =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 35 | echo "Releasing tag $TRAVIS_TAG with Python $TRAVIS_PYTHON_VERSION" 36 | release || return 1 37 | fi 38 | fi 39 | } 40 | 41 | main "$@" || exit 1 42 | exit 0 -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | requests-mock 2 | coverage 3 | pycodestyle 4 | pytest 5 | pytest-cov 6 | pytest-sugar 7 | pylama 8 | grpcio-tools>=1.17.1 9 | wheel>=0.32.1 10 | alembic -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Click>=7.0 2 | flask>=1.0.2 3 | flask-jwt-extended>=3.14.0 4 | cryptography>=2.3.1 5 | itsdangerous>=0.24 6 | flask-caching>=1.4.0 7 | cherrypy>=18.0.1 8 | wsgi-request-logger>=0.4.6 9 | Flask-SQLAlchemy>=2.3.2 10 | Flask-Dance>=1.2.0 11 | Flask-Dance[sqla]>=1.2.0 12 | Flask-Login>=0.4.1 13 | simplejson>=3.15.0 14 | sqlalchemy>=1.2.8 15 | sqlalchemy-utils>=0.33.3 16 | sqlalchemy-json>=0.2.1 17 | grpcio>=1.17.1 18 | apispec>=0.39.0 19 | marshmallow-sqlalchemy>=0.15.0 20 | chaosplatform-grpc>=0.1.1 21 | chaosplatform-relational-storage>=0.2.2 22 | chaosplatform-auth>=0.3.0 23 | chaosplatform-account>=0.2.0 24 | chaosplatform-experiment>=0.2.0 25 | chaosplatform-scheduling>=0.2.0 26 | chaosplatform-scheduler>=0.2.0 27 | toml>=0.10.0 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test = pytest 3 | 4 | [metadata] 5 | license_file = LICENSE 6 | 7 | [bdist_wheel] 8 | universal = 0 9 | 10 | [pylama] 11 | format = pylint 12 | skip = */migrations/* 13 | 14 | [pylama:pycodestyle] 15 | max_line_length = 80 16 | 17 | [tool:pytest] 18 | testpaths = tests 19 | norecursedirs = 20 | migrations 21 | 22 | python_files = 23 | test_*.py 24 | *_test.py 25 | tests.py 26 | addopts = 27 | -v 28 | -rxs 29 | --junitxml=test-results.xml 30 | --cov=chaosplatform 31 | --cov-report term-missing:skip-covered 32 | --cov-report xml 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | from distutils.errors import DistutilsFileError 4 | import io 5 | import re 6 | from os.path import abspath, dirname, isdir, join, normpath 7 | import sys 8 | 9 | import setuptools 10 | import setuptools.command.build_py 11 | from setuptools import find_packages, setup 12 | 13 | 14 | def get_version_from_package() -> str: 15 | """ 16 | Read the package version from the source without importing it. 17 | """ 18 | path = join(dirname(__file__), "chaosplatform/__init__.py") 19 | path = normpath(abspath(path)) 20 | with open(path) as f: 21 | for line in f: 22 | if line.startswith("__version__"): 23 | token, version = line.split(" = ", 1) 24 | version = version.replace("'", "").strip() 25 | return version 26 | 27 | # since our ui assets live outside this package, we can't rely on any of the 28 | # setuptools configuration settings to copy them. Let's do it manually. 29 | # I'd rather not but this is what it is... 30 | UI_ASSETS_DIR = normpath( 31 | join(dirname(__file__), "..", "chaosplatform-dashboard", "dist")) 32 | 33 | class Builder(setuptools.command.build_py.build_py): 34 | def run(self): 35 | if not self.dry_run: 36 | ui_dir = join(self.build_lib, 'chaosplatform/ui') 37 | if not isdir(UI_ASSETS_DIR): 38 | raise DistutilsFileError( 39 | "Make sure you build the UI assets before creating this package") 40 | self.copy_tree(UI_ASSETS_DIR, ui_dir) 41 | setuptools.command.build_py.build_py.run(self) 42 | 43 | 44 | def read(*names, **kwargs) -> str: 45 | return io.open( 46 | join(dirname(__file__), *names), 47 | encoding=kwargs.get('encoding', 'utf8') 48 | ).read() 49 | 50 | 51 | needs_pytest = set(['pytest', 'test']).intersection(sys.argv) 52 | pytest_runner = ['pytest_runner'] if needs_pytest else [] 53 | test_require = [] 54 | with io.open('requirements-dev.txt') as f: 55 | test_require = [l.strip() for l in f if not l.startswith('#')] 56 | 57 | install_require = [] 58 | with io.open('requirements.txt') as f: 59 | install_require = [l.strip() for l in f if not l.startswith('#')] 60 | 61 | 62 | setup( 63 | cmdclass={ 64 | 'build_py': Builder, 65 | }, 66 | name='chaosplatform', 67 | version=get_version_from_package(), 68 | license='Apache Software License 2.0', 69 | description='The control plane of the Chaos Platform', 70 | long_description=read("README.md"), 71 | long_description_content_type='text/markdown', 72 | author='ChaosIQ', 73 | author_email='contact@chaosiq.io', 74 | url='https://github.com/chaostoolkit/chaoshub', 75 | packages=find_packages(), 76 | include_package_data=True, 77 | install_requires=install_require, 78 | tests_require=test_require, 79 | setup_requires=pytest_runner, 80 | zip_safe=False, 81 | python_requires='>=3.6.*', 82 | project_urls={ 83 | 'CI: Travis': 'https://travis-ci.org/chaostoolkit/chaosplatform', 84 | 'Docs: RTD': 'https://docs.chaosplatform.org', 85 | 'GitHub: issues': 'https://chaostoolkit/chaostoolkit/chaosplatform/issues', 86 | 'GitHub: repo': 'https://chaostoolkit/chaostoolkit/chaosplatform' 87 | }, 88 | classifiers=[ 89 | 'Development Status :: 4 - Beta', 90 | 'Intended Audience :: Developers', 91 | 'License :: OSI Approved :: Apache Software License', 92 | 'Operating System :: Unix', 93 | 'Operating System :: POSIX', 94 | 'Programming Language :: Python', 95 | 'Programming Language :: Python :: 3', 96 | 'Programming Language :: Python :: 3.6', 97 | 'Programming Language :: Python :: 3.7', 98 | 'Programming Language :: Python :: 3 :: Only', 99 | 'Programming Language :: Python :: Implementation :: CPython' 100 | ], 101 | entry_points={ 102 | 'console_scripts': [ 103 | 'chaosplatform = chaosplatform.cli:cli', 104 | ] 105 | } 106 | ) 107 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import logging 3 | from logging import StreamHandler 4 | import os 5 | from typing import Any, Dict, Tuple 6 | from unittest.mock import patch, MagicMock 7 | import uuid 8 | from uuid import UUID 9 | 10 | import pytest 11 | 12 | from chaosplatform.settings import load_settings 13 | 14 | cur_dir = os.path.abspath(os.path.dirname(__file__)) 15 | fixtures_dir = os.path.join(cur_dir, "fixtures") 16 | config_path = os.path.join(fixtures_dir, 'testconfig.toml') 17 | 18 | 19 | @pytest.fixture 20 | def config() -> Dict[str, Any]: 21 | return load_settings(config_path) 22 | -------------------------------------------------------------------------------- /tests/fixtures/dummy-logging.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | 4 | "formatters": { 5 | "standard": { 6 | "format": "%(asctime)s.%(msecs)03d [%(levelname)s] %(message)s", 7 | "datefmt": "%Y-%m-%dT%H:%M:%S" 8 | } 9 | }, 10 | "handlers": { 11 | "default": { 12 | "level":"ERROR", 13 | "class":"logging.StreamHandler", 14 | "formatter": "standard", 15 | "stream": "ext://sys.stdout" 16 | } 17 | }, 18 | "loggers": { 19 | "": { 20 | "handlers": ["default"], 21 | "level": "INFO", 22 | "propagate": false 23 | }, 24 | "chaosplatform": { 25 | "handlers": ["default"], 26 | "level": "ERROR" , 27 | "propagate": false 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /tests/fixtures/testconfig.toml: -------------------------------------------------------------------------------- 1 | [chaosplatform] 2 | debug = false 3 | 4 | [chaosplatform.grpc] 5 | address = "0.0.0.0:50051" 6 | 7 | [chaosplatform.http] 8 | address = "0.0.0.0:8090" 9 | secret_key = "" 10 | 11 | [chaosplatform.http.cherrypy] 12 | environment = "production" 13 | proxy = "http://localhost:6080" 14 | 15 | [chaosplatform.cache] 16 | type = "simple" 17 | 18 | # Only set if type is set to "redis" 19 | # [chaosplatform.cache.redis] 20 | # host = "localhost" 21 | # port = 6379 22 | 23 | [chaosplatform.db] 24 | uri = "sqlite:///:memory" 25 | 26 | [chaosplatform.jwt] 27 | secret_key = "" 28 | public_key = "" 29 | algorithm = "HS256" 30 | identity_claim_key = "identity" 31 | user_claims_key = "user_claims" 32 | access_token_expires = 2592000 33 | refresh_token_expires = 31536000 34 | user_claims_in_refresh_token = false 35 | 36 | [chaosplatform.account] 37 | 38 | [chaosplatform.auth] 39 | [chaosplatform.auth.oauth2] 40 | [chaosplatform.auth.oauth2.github] 41 | client_id = "" 42 | client_secret = "" 43 | 44 | [chaosplatform.auth.grpc] 45 | [chaosplatform.auth.grpc.account] 46 | address = "0.0.0.0:50051" 47 | 48 | [chaosplatform.experiment] 49 | 50 | [chaosplatform.scheduling] 51 | [chaosplatform.scheduling.grpc] 52 | [chaosplatform.scheduling.grpc.scheduler] 53 | address = "0.0.0.0:50051" 54 | 55 | [chaosplatform.scheduler] 56 | [chaosplatform.scheduler.redis] 57 | host = "localhost" 58 | port = 6379 59 | queue = "chaosplatform" 60 | 61 | [chaosplatform.scheduler.job] 62 | platform_url = "http://127.0.0.1:6080" 63 | 64 | [chaosplatform.scheduler.worker] 65 | debug = false 66 | count = 3 67 | queue_name = "chaosplatform" 68 | worker_name = "chaosplatform-worker" 69 | add_random_suffix_to_worker_name = true 70 | worker_directory = "/tmp" 71 | 72 | [chaosplatform.scheduler.worker.redis] 73 | host = "localhost" 74 | port = 6379 -------------------------------------------------------------------------------- /tests/test_log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from logging import StreamHandler 4 | from typing import Any, Dict 5 | from unittest.mock import MagicMock 6 | 7 | from requestlogger import ApacheFormatter, WSGILogger 8 | 9 | from chaosplatform.log import clean_logger, configure_logger, \ 10 | http_requests_logger 11 | 12 | 13 | def test_configure_logger_with_embedded_config(config: Dict[str, Any]): 14 | logger = logging.getLogger("chaosplatform") 15 | assert len(logger.handlers) == 0 16 | 17 | try: 18 | configure_logger(None, config) 19 | assert logger.propagate is False 20 | assert logger.level == logging.INFO 21 | assert len(logger.handlers) == 1 22 | h = logger.handlers[0] 23 | assert isinstance(h, StreamHandler) 24 | assert h.level == logging.INFO 25 | finally: 26 | clean_logger() 27 | 28 | 29 | 30 | def test_override_log_level(config: Dict[str, Any]): 31 | config = config.copy() 32 | config["debug"] = True 33 | 34 | logger = logging.getLogger("chaosplatform") 35 | assert len(logger.handlers) == 0 36 | 37 | try: 38 | configure_logger(None, config) 39 | assert logger.propagate is False 40 | assert logger.level == logging.DEBUG 41 | 42 | assert len(logger.handlers) == 1 43 | h = logger.handlers[0] 44 | assert isinstance(h, StreamHandler) 45 | assert h.level == logging.DEBUG 46 | finally: 47 | clean_logger() 48 | 49 | 50 | def test_configure_logger_with_specific_config(config: Dict[str, Any]): 51 | logger = logging.getLogger("chaosplatform") 52 | assert len(logger.handlers) == 0 53 | 54 | try: 55 | p = os.path.join( 56 | os.path.dirname(__file__), "fixtures", "dummy-logging.json") 57 | configure_logger(p, config) 58 | assert logger.propagate is False 59 | assert logger.level == logging.ERROR 60 | assert len(logger.handlers) == 1 61 | h = logger.handlers[0] 62 | assert isinstance(h, StreamHandler) 63 | assert h.level == logging.ERROR 64 | finally: 65 | clean_logger() 66 | 67 | 68 | def test_http_requests_logger(): 69 | app = MagicMock() 70 | wsgi_logger = http_requests_logger(app) 71 | assert isinstance(wsgi_logger, WSGILogger) --------------------------------------------------------------------------------