├── .flake8 ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .readthedocs.yaml ├── CHANGELOG.md ├── INSTRUCTIONS.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── __init__.py ├── contrib ├── docker │ ├── Dockerfile │ ├── discord_config.py │ ├── docker_build_installer │ ├── gitter_config.py │ ├── mattermost_config.py │ ├── rocketchat_config.py │ └── slack_config.py ├── packaging │ ├── build │ ├── build.cfg │ ├── build_archive │ ├── requirements.txt │ └── templates │ │ ├── redhat │ │ └── err-stackstorm.spec │ │ └── ubuntu │ │ └── debian │ │ ├── changelog │ │ ├── compat │ │ ├── control │ │ ├── copyright │ │ ├── err-stackstorm.service │ │ ├── patches │ │ └── .placeholder │ │ ├── postinst │ │ ├── postrm │ │ ├── preinst │ │ ├── prerm │ │ ├── rules │ │ ├── source │ │ └── format │ │ └── watch ├── stackstorm-chatops │ └── rules │ │ └── notify_errbot.yaml └── systemd │ └── errbot.service ├── docs ├── Makefile ├── action_aliases.rst ├── authn.rst ├── authz.rst ├── conf.py ├── configuration.rst ├── dev_references.rst ├── getting_started.rst ├── images │ ├── authentication_screen.jpg │ └── remote_shell.jpg ├── index.rst ├── installation.rst ├── linux_package_building.rst ├── make.bat ├── project.rst ├── quick_start.rst ├── show_case.rst └── troubleshooting.rst ├── pyproject.toml ├── requirements-build.txt ├── requirements-docs.txt ├── requirements-test.txt ├── src └── err-stackstorm │ ├── errst2lib │ ├── __init__.py │ ├── authentication_controller.py │ ├── authentication_handler.py │ ├── chat_adapters.py │ ├── config.py │ ├── credentials_adapters.py │ ├── enquiry.py │ ├── errors.py │ ├── session.py │ ├── session_manager.py │ ├── stackstorm_api.py │ ├── store_adapters.py │ └── version.py │ ├── html │ ├── errbot_icon.png │ ├── index.html │ └── st2_logo.png │ ├── st2.plug │ └── st2.py └── tests ├── disable_enquiry.py ├── test_access_control.py ├── test_auth_handlers.py ├── test_bot_commands.py ├── test_chat_adapters.py ├── test_credentials_manager.py ├── test_secret_store.py ├── test_session.py └── test_session_manager.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | workflow_dispatch: 12 | 13 | jobs: 14 | tests: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: 19 | - "3.9" 20 | - "3.10" 21 | - "3.11" 22 | - "3.12" 23 | steps: 24 | - name: Repository Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: "Set up Python ${{ matrix.python-version }}" 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: "${{ matrix.python-version }}" 31 | 32 | - name: Install requirements 33 | run: | 34 | make -s setup 35 | 36 | - name: Linting 37 | run: | 38 | make -s auto_format 39 | make -s lint_test 40 | 41 | - name: Unit Tests 42 | run: | 43 | make -s unit_test 44 | 45 | - name: Security Scan 46 | run: | 47 | make -s security_scan 48 | 49 | build: 50 | runs-on: ubuntu-latest 51 | 52 | steps: 53 | - uses: actions/checkout@v2 54 | 55 | - name: Install requirements 56 | run: | 57 | make setup 58 | 59 | - name: Build pypi 60 | run: | 61 | make build_python_package 62 | make publish_pypi 63 | 64 | - name: Build documentaiton 65 | run: | 66 | make build_documentation 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | 4 | # Custom 5 | html/ 6 | untracked/ 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *,cover 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | 60 | # Sphinx documentation 61 | docs/_build/ 62 | 63 | # PyBuilder 64 | target/ 65 | 66 | 67 | ### VirtualEnv template 68 | # Virtualenv 69 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 70 | .Python 71 | [Bb]in 72 | [Ii]nclude 73 | Lib 74 | [Ss]cripts 75 | pyvenv.cfg 76 | pip-selfcheck.json 77 | 78 | .idea 79 | 80 | ### Package building directory 81 | pkg-build/ 82 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | # python: 21 | # install: 22 | # - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [2.3.0] Unreleased 8 | ### Added 9 | - Proof of concept for enquiry support. 10 | - Add Python 3.12 to test matrix. 11 | - Option to use an empty plugin_prefix 12 | 13 | ### Changed 14 | - Restructured source layout for pypi packaging. 15 | - Restructured and updated documentation. 16 | - Switched from setup.py to pyproject.toml 17 | - Updated package build process to support Rocky linux and base on major version of OS. 18 | - Use plugin_prefix in help command hint 19 | 20 | ### Removed 21 | - Removed `check_latest_version` function. 22 | - Removed CircleCI from project in favour of using Github Actions. 23 | - Remove Python 3.7 & 3.8 from test matrix. 24 | 25 | ## [2.2.0] 2021-11-27 26 | ### Added 27 | - support for slackv3 backend and passing action-alias extra parameters as Slack block or attachment. 28 | - documentation to use Slack blocks or attachments. 29 | - tests for Python from 3.6 to 3.10 30 | 31 | ### Changed 32 | - hard coded notification_route to use user configured route when calling match_and_execute. 33 | 34 | ### Removed 35 | - logging sensitive api tokens in debug logs. 36 | - "" for action execution help text. 37 | 38 | ## [2.1.4] 2020-08-14 39 | ### Added 40 | - Session are deleted automatically when a chat user fails to authenticate against St2 API. 41 | - Include Slack users "display name" in session list for easier identification. 42 | 43 | ### Changed 44 | - Update documentation with corrections and improved examples. 45 | - Corrected error of dynamic commands not registering correctly when the plugin was deactivated/activated. 46 | - Improved robustness of version check code to gracefully handle github.com being unavailable. 47 | 48 | ## [2.1.3] 2020-01-10 49 | ### Added 50 | - Added err-stackstorm version in the st2help display. 51 | 52 | ### Changed 53 | - Fixed st2help function to have to correct number of positional arguments. 54 | 55 | ## [2.1.2] 2019-11-24 56 | ### Added 57 | - Added CircleCI build badge to README. 58 | - Added route key to be defined by user to allow mutliple bots attached to a single StackStorm instance. 59 | - Added version check on start-up to log if newer version of the plugin are available. 60 | 61 | ### Changed 62 | - Updated curl example in troubleshooting section. 63 | - Changed all bot commands to be registered dynamically to allow user defined plugin prefix. 64 | 65 | ## [2.1.1] 2019-07-19 66 | ### Added 67 | - Added configuration for mattermost, rocketchat, discord, gitter and slack to docker build. 68 | 69 | ### Changed 70 | - Improved Discord Chat Adapter formatting. 71 | - Corrected configuration name handling which caused exceptions to be raised. 72 | - Updated documentation to direct readers to readthedocs. 73 | 74 | ### Removed 75 | - Removed old readme file. 76 | 77 | ## [2.1.0] 2019-07-08 78 | ### Added 79 | - Added `deactivate` method to correctly handle errbot reload and stop process. 80 | - Added initial files for documentation to be served on readthedocs. 81 | - Added missing documentation for nginx configuration when using Client-side authentication. 82 | - Added support for IRC and Discord Chat Adapters. 83 | - Added source documentation for ReadTheDocs. 84 | 85 | ### Changed 86 | - Correctly detect when the `extra` attribute is passed to the Slack Chat Adapter. 87 | - Send web login form data as JSON to be compatible with errbot >=v6.0. 88 | 89 | ### Removed 90 | 91 | ## [2.1.0-rc2] 2019-05-29 92 | ### Added 93 | - Split unit tests in separate test files. 94 | - Added linting to CircleCI 95 | - Added try/except blocks around plugin configuration code to improve installation and startup 96 | experience when there are errors in the configuration. 97 | 98 | ### Changed 99 | - Fixed dictionary keys to reference 'apikey' consistently. 100 | - Switch CircleCI from calling pytest directly to using Makefile. 101 | - Dropped using sseclient-py in favour of btubbs sseclient. 102 | - Corrected references to apikey configuration. 103 | 104 | ### Removed 105 | 106 | ## [2.1.0-rc1] 2019-05-15 107 | ### Added 108 | - unit tests for more low level objects used by err-stackstorm. 109 | 110 | ### Changed 111 | - updated makefile to be better adapted to CircleCI's build process. (Still a work in progress) 112 | - numerous documentation updates to improve installation process for new users. 113 | - documented errbot's ACL features. 114 | 115 | ### Removed 116 | - removed unused code for keyring and vault. 117 | 118 | ## [2.0.0-rc2] 2019-02-08 119 | ### Added 120 | - Correctly support action-alias acknowledge enabled flag in StackStorm API. 121 | - Support appending the execution `web_url` to action-alias acknowledgement message. 122 | 123 | ### Changed 124 | - Fixed exception raise in log message when err-stackstorm isn't able to fetch a valid StackStorm user token. 125 | 126 | ### Removed 127 | - removed dependency for st2client to be able to use requests>=2.20. This avoid conflicts with popular chat backends. 128 | 129 | ## [2.0.0-rc1] 2019-01-28 130 | ### Added 131 | - setup.py to be able to install plugin from the command line. 132 | - Dockerfile to build an image and run err-stackstorm from within a Docker container. (alpine linux based) 133 | - Support for Out-of-Bands authentication. 134 | - Support for formatting with XMMP backend. 135 | 136 | ### Changed 137 | - Updated err-stackstorm python module requirements. 138 | 139 | ### Removed 140 | 141 | ## [1.4.0] - 2018-04-07 142 | ### Added 143 | 144 | 145 | ### Changed 146 | - Readme spelling corrections. 147 | - Aligned `api_url`, `auth_url` and `stream_url` with the [StackStorm API](https://api.stackstorm.com/) 148 | 149 | 150 | ### Removed 151 | - remove `base_url` and `api_version`, they're now calculated from the `api_url`, `auth_url` or `stream_url`. 152 | -------------------------------------------------------------------------------- /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 2016-2022 Carlos and err-stackstorm contributors 190 | Copyright 2015 葫芦娃 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include src/err-stackstorm/*.plug 2 | include src/err-stackstorm/html/* 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/sh 2 | MAX_LINE_LEN=100 3 | SOURCE_PATH=src/err-stackstorm 4 | LIB_PATH=${SOURCE_PATH}/errst2lib 5 | TESTS_PATH=tests 6 | 7 | # ANSI colour formats for help output. 8 | FMT_TITLE=\e[34;40;1m 9 | FMT_TARGET=\e[37;0;1m 10 | FMT_NONE=\e[0m 11 | 12 | .PHONY: all 13 | all: auto_format format_test lint_test unit_test security_scan 14 | 15 | .PHONY: clean # Remove tmp files and previous build artifacst. (Not Implemented) 16 | clean: 17 | @echo Cleaning - N/A 18 | 19 | .PHONY: setup # Install errbot and dependencies into virtual environment. 20 | setup: 21 | test -n "${CI}" || ( test -n "${VIRTUAL_ENV}" || (echo "Not running in virtualenv/CI - abort setup"; exit 1 ) ) && echo "Running in virtual environment or CI pipeline" 22 | pip install --upgrade pip 23 | pip install errbot 24 | errbot --init 25 | pip install . 26 | pip install -r requirements-test.txt 27 | pip install -r requirements-build.txt 28 | 29 | .PHONY: build_python_package # Build python deployment packages. 30 | build_python_package: 31 | echo "Build python package" 32 | python -m build 33 | 34 | .PHONY: publish_pypi # Push python deployment packages to pypi. (Not Implemented) 35 | publish_pypi: 36 | echo "Publish python packages to pypi" 37 | echo TO DO: python -m twine upload err-stackstorm dist/* 38 | 39 | .PHONY: build_documentation # Generate readthedocs documentation. (Not Implemented) 40 | documentation: 41 | echo "Build documentation" 42 | echo TO DO - trigger readthedocs. 43 | 44 | .PHONY: format_test # Run black formatting check over source files. 45 | format_test: 46 | echo "Formatting code\n" 47 | black --check --line-length=${MAX_LINE_LEN} ${SOURCE_PATH}/st2.py ${LIB_PATH}/*.py ${TESTS_PATH}/*.py 48 | 49 | .PHONY: auto_format # Apply black format against python source files. 50 | auto_format: 51 | echo "Formatting code\n" 52 | black --line-length=${MAX_LINE_LEN} ${SOURCE_PATH}/st2.py ${LIB_PATH}/*.py ${TESTS_PATH}/*.py 53 | 54 | .PHONY: security_scan # Check python source code for security issues. 55 | security_scan: 56 | echo "Scanning for potential security issues\n" 57 | bandit ${SOURCE_PATH}/*.py ${LIB_PATH}/*.py 58 | 59 | .PHONY: unit_test # Run Unit tests using pytest. 60 | unit_test: 61 | echo "Running Python unit tests\n" 62 | python -m pytest 63 | 64 | .PHONY: lint_test # Run flake and pycodestyle tests on source files. 65 | lint_test: 66 | echo -n "Running LINT tests\n" 67 | pycodestyle --max-line-length=${MAX_LINE_LEN} ${SOURCE_PATH}/st2.py ${LIB_PATH}/*.py 68 | 69 | .PHONY: help 70 | help: 71 | echo "${FMT_TITLE}TARGET${FMT_NONE} ${FMT_TITLE}DESCRIPTION${FMT_NONE}" 72 | echo "${FMT_TARGET}clean${FMT_NONE} Remove tmp files and previous build artifacst. (Not Implemented)" 73 | echo "${FMT_TARGET}setup${FMT_NONE} Install errbot and dependencies into virtual environment." 74 | echo "${FMT_TARGET}build_python_package${FMT_NONE} Build python deployment packages." 75 | echo "${FMT_TARGET}publish_pypi${FMT_NONE} Push python deployment packages to pypi. (Not Implemented)" 76 | echo "${FMT_TARGET}build_documentation${FMT_NONE} Generate readthedocs documentation. (Not Implemented)" 77 | echo "${FMT_TARGET}format_test${FMT_NONE} Run black formatting check over source files." 78 | echo "${FMT_TARGET}auto_format${FMT_NONE} Apply black format against python source files." 79 | echo "${FMT_TARGET}security_scan${FMT_NONE} Check python source code for security issues." 80 | echo "${FMT_TARGET}unit_test${FMT_NONE} Run Unit tests using pytest." 81 | echo "${FMT_TARGET}lint_test${FMT_NONE} Run flake and pycodestyle tests on source files." 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # err-stackstorm 2 | 3 | StackStorm ChatOps for Errbot. 4 | 5 | [![CI](https://github.com/nzlosh/err-stackstorm/actions/workflows/main.yml/badge.svg)](https://github.com/nzlosh/err-stackstorm/actions/workflows/main.yml)[![Documentation Status](https://readthedocs.org/projects/err-stackstorm/badge/?version=latest)](https://err-stackstorm.readthedocs.io/en/latest/?badge=latest) 6 | 7 | For quick start, installation, troubleshooting and more [visit the documentation website](https://err-stackstorm.readthedocs.io/) 8 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | from st2 import St2 3 | -------------------------------------------------------------------------------- /contrib/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.19 as stage1 2 | MAINTAINER "nzlosh@yahoo.com" 3 | 4 | ENV ERRBOT_USER=errbot 5 | 6 | COPY docker_build_installer /root 7 | RUN sh /root/docker_build_installer stage1 8 | 9 | 10 | FROM alpine:3.19 11 | COPY docker_build_installer /root 12 | RUN sh /root/docker_build_installer stage2 13 | COPY --from=stage1 /opt/errbot /opt/errbot 14 | COPY gitter_config.py /opt/errbot 15 | COPY mattermost_config.py /opt/errbot 16 | COPY rocketchat_config.py /opt/errbot 17 | COPY slack_config.py /opt/errbot 18 | COPY discord_config.py /opt/errbot 19 | 20 | # Export plugins as a volume so host can read plugin static web content from err-stackstorm. 21 | VOLUME "/opt/errbot/plugins" 22 | 23 | ENTRYPOINT /bin/sh 24 | -------------------------------------------------------------------------------- /contrib/docker/discord_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | BACKEND = 'Discord' 3 | BOT_ROOT_DIR = "/opt/errbot" 4 | BOT_DATA_DIR = f"{BOT_ROOT_DIR}/data" 5 | BOT_EXTRA_BACKEND_DIR = f"{BOT_ROOT_DIR}/backends/" 6 | BOT_EXTRA_PLUGIN_DIR = f"{BOT_ROOT_DIR}/plugins/" 7 | BOT_LOG_FILE = f"{BOT_ROOT_DIR}/log/err.log" 8 | BOT_LOG_LEVEL = logging.DEBUG 9 | BOT_LOG_SENTRY = False 10 | SENTRY_DSN = '' 11 | SENTRY_LOGLEVEL = 1 12 | BOT_ASYNC = True 13 | 14 | BOT_IDENTITY = { 15 | 'token': '', 16 | } 17 | 18 | BOT_ADMINS = (['']) 19 | CHATROOM_PRESENCE = ([""]) 20 | CHATROOM_FN = '' 21 | BOT_PREFIX = '!' 22 | 23 | DIVERT_TO_PRIVATE = () 24 | CHATROOM_RELAY = {} 25 | REVERSE_CHATROOM_RELAY = {} 26 | 27 | # Err-StackStorm 28 | STACKSTORM = { 29 | 'auth_url': 'https:///auth/v1', 30 | 'api_url': 'https:///api/v1', 31 | 'stream_url': 'https:///stream/v1', 32 | 33 | 'verify_cert': False, 34 | 'secrets_store': 'cleartext', 35 | 'api_auth': { 36 | 'user': { 37 | 'name': '', 38 | 'password': "", 39 | }, 40 | }, 41 | 'rbac_auth': { 42 | 'standalone': {} 43 | }, 44 | 'timer_update': 900, # Unit: second. Bot token renewal interval. 45 | } 46 | -------------------------------------------------------------------------------- /contrib/docker/docker_build_installer: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export ERRBOT_ROOT=/opt/errbot 3 | export ERRBOT_USER=errbot 4 | 5 | function setup_account 6 | { 7 | echo -e "\e[93mUser/Group\e[0m" 8 | mkdir -p ${ERRBOT_ROOT} 9 | addgroup ${ERRBOT_USER} 10 | addgroup ${ERRBOT_USER} ${ERRBOT_USER} 11 | adduser -S -g ${ERRBOT_USER} -h ${ERRBOT_ROOT} -s /bin/false ${ERRBOT_USER} 12 | chown -R ${ERRBOT_USER}:${ERRBOT_USER} ${ERRBOT_ROOT} 13 | id ${ERRBOT_USER} 14 | ls -ld ${ERRBOT_ROOT} 15 | } 16 | 17 | function install_prod_packages 18 | { 19 | echo -e "\e[93mPackage\e[0m" 20 | apk add --no-cache py3-virtualenv python3 git openssl rsync 21 | } 22 | 23 | function install_build_packages 24 | { 25 | install_prod_packages 26 | apk add --no-cache gcc python3-dev openssl-dev libffi-dev musl-dev 27 | python3 -m ensurepip 28 | rm -r /usr/lib/python*/ensurepip 29 | test ! -e /usr/bin/pip && ln -s pip3 /usr/bin/pip 30 | test ! -e /usr/bin/python && ln -sf /usr/bin/python3 /usr/bin/python 31 | test -d /root/.cache && rm -r /root/.cache 32 | } 33 | 34 | function install_openssh 35 | { 36 | echo -e "\e[93mCopy virtualenv to final image\e[0m" 37 | apk add openssh 38 | ssh-keygen -b 4096 -t ed25519 -f /etc/ssh/ssh_host_ed25519_key 39 | echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config 40 | 41 | } 42 | 43 | function setup_virtual_env 44 | { 45 | echo -e "\e[93mVirtual Environment\e[0m" 46 | virtualenv -p python3 ${ERRBOT_ROOT} 47 | source ${ERRBOT_ROOT}/bin/activate 48 | } 49 | 50 | function install_plugin 51 | { 52 | GIT_URL="$(echo ${1} | cut -d@ -f1)" 53 | GIT_BRANCH="$(echo ${1} | cut -d@ -f2)" 54 | PLUGIN_DIR="$(basename ${GIT_URL} | cut -d. -f1)" 55 | cd ${ERRBOT_ROOT}/plugins 56 | git clone $GIT_URL 57 | cd $PLUGIN_DIR 58 | git checkout $GIT_BRANCH 59 | pip install -r requirements.txt 60 | } 61 | 62 | function install_backend 63 | { 64 | GIT_URL="$(echo ${1} | cut -d@ -f1)" 65 | GIT_BRANCH="$(echo ${1} | cut -d@ -f2)" 66 | BACKEND_DIR="$(basename ${GIT_URL} | cut -d. -f1)" 67 | cd ${ERRBOT_ROOT}/backends 68 | git clone $GIT_URL 69 | cd $BACKEND_DIR 70 | git checkout $GIT_BRANCH 71 | pip install -r requirements.txt 72 | } 73 | 74 | function initialise_errbot 75 | { 76 | echo -e "\e[93mErrbot initialisation\e[0m" 77 | cd ${ERRBOT_ROOT} 78 | errbot --init 79 | rm -rf /opt/errbot/plugins/err-example 80 | } 81 | 82 | function install_errbot 83 | { 84 | echo -e "\e[93mErrbot plugins installation\e[0m" 85 | pip3 install --upgrade pip setuptools 86 | pip3 install errbot==6.2.0 slixmpp IRC errbot-backend-slackv3 err-backend-mattermost err-backend-discord 87 | initialise_errbot 88 | for dirname in plugins backends log 89 | do 90 | mkdir -p ${ERRBOT_ROOT}/${dirname} 91 | done 92 | for plugin in 'https://github.com/nzlosh/err-stackstorm.git' 93 | do 94 | install_plugin $plugin 95 | done 96 | 97 | for backend in 'https://github.com/nzlosh/err-backend-rocketchat.git@maint_nzlosh' \ 98 | 'https://github.com/nzlosh/err-backend-gitter.git@maint_nzlosh' 99 | do 100 | install_backend $backend 101 | done 102 | } 103 | 104 | function cleanup 105 | { 106 | test -f /root/docker_build_installer && rm /root/docker_build_installer 107 | } 108 | 109 | function stage1_build 110 | { 111 | setup_account 112 | install_build_packages 113 | setup_virtual_env 114 | install_errbot 115 | } 116 | 117 | function stage2_build 118 | { 119 | setup_account 120 | install_prod_packages 121 | install_openssh 122 | } 123 | 124 | if [ "$1" == "stage1" ] 125 | then 126 | stage1_build 127 | else 128 | stage2_build 129 | cleanup 130 | fi 131 | -------------------------------------------------------------------------------- /contrib/docker/gitter_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | BACKEND = 'Gitter' 3 | BOT_ROOT_DIR = "/opt/errbot" 4 | BOT_DATA_DIR = f"{BOT_ROOT_DIR}/data" 5 | BOT_EXTRA_BACKEND_DIR = f"{BOT_ROOT_DIR}/backends/" 6 | BOT_EXTRA_PLUGIN_DIR = f"{BOT_ROOT_DIR}/plugins/" 7 | BOT_LOG_FILE = f"{BOT_ROOT_DIR}/log/err.log" 8 | BOT_LOG_LEVEL = logging.DEBUG 9 | BOT_LOG_SENTRY = False 10 | SENTRY_DSN = '' 11 | SENTRY_LOGLEVEL = 1 12 | BOT_ASYNC = True 13 | 14 | # Gitter 15 | GITTER = { 16 | 'token': '', 17 | } 18 | BOT_IDENTITY = GITTER 19 | 20 | BOT_ADMINS = (['']) 21 | CHATROOM_PRESENCE = ([""]) 22 | CHATROOM_FN = '' 23 | BOT_PREFIX = '!' 24 | 25 | DIVERT_TO_PRIVATE = () 26 | CHATROOM_RELAY = {} 27 | REVERSE_CHATROOM_RELAY = {} 28 | 29 | # Err-StackStorm 30 | STACKSTORM = { 31 | 'auth_url': 'https:///auth/v1', 32 | 'api_url': 'https:///api/v1', 33 | 'stream_url': 'https:///stream/v1', 34 | 35 | 'verify_cert': False, 36 | 'secrets_store': 'cleartext', 37 | 'api_auth': { 38 | 'user': { 39 | 'name': '', 40 | 'password': "", 41 | }, 42 | }, 43 | 'rbac_auth': { 44 | 'standalone': {} 45 | }, 46 | 'timer_update': 900, # Unit: second. Bot token renewal interval. 47 | } 48 | -------------------------------------------------------------------------------- /contrib/docker/mattermost_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | BACKEND = 'Mattermost' 3 | BOT_ROOT_DIR = "/opt/errbot" 4 | BOT_DATA_DIR = f"{BOT_ROOT_DIR}/data" 5 | BOT_EXTRA_BACKEND_DIR = f"{BOT_ROOT_DIR}/backends/" 6 | BOT_EXTRA_PLUGIN_DIR = f"{BOT_ROOT_DIR}/plugins/" 7 | BOT_LOG_FILE = f"{BOT_ROOT_DIR}/log/err.log" 8 | BOT_LOG_LEVEL = logging.DEBUG 9 | BOT_LOG_SENTRY = False 10 | SENTRY_DSN = '' 11 | SENTRY_LOGLEVEL = 1 12 | BOT_ASYNC = True 13 | 14 | # Mattermost 15 | MATTERMOST = { 16 | # Required 17 | 'team': '', 18 | 'server': '', 19 | # For the login, either 20 | 'login': '', 21 | 'password': '', 22 | # Or, if you have a personal access token 23 | #'token': '', 24 | # Optional 25 | 'insecure': False, # Default = False. Set to true for self signed certificates 26 | 'scheme': 'http', # Default = https 27 | 'port': 8065, # Default = 8065 28 | 'timeout': 30, # Default = 30. If the webserver disconnects idle connections later/earlier change this value 29 | 'cards_hook': 'incomingWebhookId' # Needed for cards/attachments 30 | } 31 | BOT_IDENTITY=MATTERMOST 32 | 33 | BOT_ADMINS = (['']) 34 | CHATROOM_PRESENCE = ([""]) 35 | CHATROOM_FN = '' 36 | BOT_PREFIX = '!' 37 | 38 | DIVERT_TO_PRIVATE = () 39 | CHATROOM_RELAY = {} 40 | REVERSE_CHATROOM_RELAY = {} 41 | 42 | # Err-StackStorm 43 | STACKSTORM = { 44 | 'auth_url': 'https:///auth/v1', 45 | 'api_url': 'https:///api/v1', 46 | 'stream_url': 'https:///stream/v1', 47 | 48 | 'verify_cert': False, 49 | 'secrets_store': 'cleartext', 50 | 'api_auth': { 51 | 'user': { 52 | 'name': '', 53 | 'password': "", 54 | }, 55 | }, 56 | 'rbac_auth': { 57 | 'standalone': {} 58 | }, 59 | 'timer_update': 900, # Unit: second. Bot token renewal interval. 60 | } 61 | -------------------------------------------------------------------------------- /contrib/docker/rocketchat_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | BACKEND = 'RocketChat' 3 | BOT_ROOT_DIR = "/opt/errbot" 4 | BOT_DATA_DIR = f"{BOT_ROOT_DIR}/data" 5 | BOT_EXTRA_BACKEND_DIR = f"{BOT_ROOT_DIR}/backends/" 6 | BOT_EXTRA_PLUGIN_DIR = f"{BOT_ROOT_DIR}/plugins/" 7 | BOT_LOG_FILE = f"{BOT_ROOT_DIR}/log/err.log" 8 | BOT_LOG_LEVEL = logging.DEBUG 9 | BOT_LOG_SENTRY = False 10 | SENTRY_DSN = '' 11 | SENTRY_LOGLEVEL = 1 12 | BOT_ASYNC = True 13 | 14 | class ROCKETCHAT_CONFIG(object): 15 | """ 16 | Config object for AoikRocketChatErrbot. 17 | Config values can be overridden by env variables. Config key `SERVER_URI` 18 | maps to env variable name `AOIKROCKETCHATERRBOT_SERVER_URI`. Use string 19 | '0', 'false' or 'no' to mean boolean false in env variable value. 20 | """ 21 | SERVER_URI = 'ws://:3000/websocket' 22 | LOGIN_USERNAME = '' 23 | LOGIN_PASSWORD = '' 24 | PATCH_METEOR_CLIENT = True 25 | RECONNECT_ENABLED = True 26 | 27 | HEARTBEAT_ENABLED = False 28 | HEARTBEAT_INTERVAL = 10 29 | @classmethod 30 | def _heartbeat_func(cls, backend): 31 | """ 32 | Heartbeat function. 33 | :param backend: Backend object. 34 | :return: None. 35 | """ 36 | msg = 'Heartbeat: {}'.format(datetime.now().strftime('%H:%M:%S')) 37 | backend.send_rocketchat_message( 38 | params={ 39 | 'rid': 'GENERAL', 40 | 'msg': msg, 41 | } 42 | ) 43 | HEARTBEAT_FUNC = _heartbeat_func 44 | 45 | BOT_ADMINS = (['']) 46 | CHATROOM_PRESENCE = ([""]) 47 | CHATROOM_FN = '' 48 | BOT_PREFIX = '!' 49 | 50 | DIVERT_TO_PRIVATE = () 51 | CHATROOM_RELAY = {} 52 | REVERSE_CHATROOM_RELAY = {} 53 | 54 | # Err-StackStorm 55 | STACKSTORM = { 56 | 'auth_url': 'https:///auth/v1', 57 | 'api_url': 'https:///api/v1', 58 | 'stream_url': 'https:///stream/v1', 59 | 60 | 'verify_cert': False, 61 | 'secrets_store': 'cleartext', 62 | 'api_auth': { 63 | 'user': { 64 | 'name': '', 65 | 'password': "", 66 | }, 67 | }, 68 | 'rbac_auth': { 69 | 'standalone': {} 70 | }, 71 | 'timer_update': 900, # Unit: second. Bot token renewal interval. 72 | } 73 | -------------------------------------------------------------------------------- /contrib/docker/slack_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | BACKEND = 'SlackV3' 3 | BOT_ROOT_DIR = "/opt/errbot" 4 | BOT_DATA_DIR = f"{BOT_ROOT_DIR}/data" 5 | BOT_EXTRA_BACKEND_DIR = f"{BOT_ROOT_DIR}/backends/" 6 | BOT_EXTRA_PLUGIN_DIR = f"{BOT_ROOT_DIR}/plugins/" 7 | BOT_LOG_FILE = f"{BOT_ROOT_DIR}/log/err.log" 8 | BOT_LOG_LEVEL = logging.DEBUG 9 | BOT_LOG_SENTRY = False 10 | SENTRY_DSN = '' 11 | SENTRY_LOGLEVEL = 1 12 | BOT_ASYNC = True 13 | 14 | BOT_ADMINS = (['']) 15 | BOT_ADMINS_NOTIFICATIONS = (['<@SLACK_ID>']) 16 | CHATROOM_PRESENCE = ([""]) 17 | CHATROOM_FN = '' 18 | BOT_PREFIX = '!' 19 | 20 | DIVERT_TO_PRIVATE = () 21 | CHATROOM_RELAY = {} 22 | REVERSE_CHATROOM_RELAY = {} 23 | 24 | # Err-StackStorm 25 | STACKSTORM = { 26 | 'auth_url': 'https:///auth/v1', 27 | 'api_url': 'https:///api/v1', 28 | 'stream_url': 'https:///stream/v1', 29 | 30 | 'verify_cert': False, 31 | 'secrets_store': 'cleartext', 32 | 'api_auth': { 33 | 'user': { 34 | 'name': '', 35 | 'password': "", 36 | }, 37 | }, 38 | 'rbac_auth': { 39 | 'standalone': {} 40 | }, 41 | 'timer_update': 900, # Unit: second. Bot token renewal interval. 42 | } 43 | -------------------------------------------------------------------------------- /contrib/packaging/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import email.utils # email.utils.formatdate() for debian changelog entries. 3 | import json 4 | import os 5 | import datetime 6 | import random 7 | import shlex 8 | import shutil 9 | import pathlib 10 | import string 11 | import sys 12 | from subprocess import run 13 | 14 | from jinja2 import FileSystemLoader 15 | from jinja2.sandbox import SandboxedEnvironment as Environment 16 | 17 | # Run after the err-stackstorm virtualenv archive has been created. 18 | # 1. install build dependencies. 19 | # 2. cp archive into build tree. 20 | # 3. run build process. 21 | # 4. push artifact to repository 22 | 23 | release_files = ["/etc/os-release"] 24 | 25 | 26 | def title(text): 27 | """ 28 | Display ANSI colour text as an execution section title. 29 | """ 30 | print("\N{ESC}[96m{}\u001b[0m".format(text)) 31 | 32 | 33 | class BuildTarget: 34 | def __init__(self, build_cfg, distro): 35 | # Runtime context contains information passed through to the templating process 36 | # in a generic way across all platforms. 37 | self.build_cfg = { 38 | "_runtime": { 39 | "rfc2822_datetime": email.utils.formatdate(), 40 | "distro": distro, 41 | "dyslexic_datetime": datetime.datetime.now().strftime("%a %h %d %Y") 42 | }, 43 | "_common": build_cfg["_common"] 44 | } 45 | self.build_cfg.update(build_cfg[distro]) 46 | self.build_cfg["_common"]["archive"]["filename"] = build_cfg["_common"]["archive"]["filename"].format( 47 | version=self.build_cfg["_common"]["version"], distro=distro 48 | ) 49 | 50 | def setup_environment(self): 51 | title("Setup build directory") 52 | 53 | build_dir = self.build_cfg["_common"]["build_dir"] 54 | self.tmp_build_dir = pathlib.Path(build_dir, temp_filename()) 55 | print(f"temp dir is {self.tmp_build_dir}") 56 | 57 | title("Setup build environment") 58 | shutil.copytree(self.build_cfg["template_dir"], self.tmp_build_dir) 59 | 60 | print("Installing required packages for build") 61 | # The build time dependencies are managed in the package specification. 62 | run([build_cfg[distro]["pkg_bin"], "-y", "install"] + build_cfg[distro]["build_deps"]) 63 | 64 | def copy_archives(self): 65 | src = os.path.join(self.build_cfg["_common"]["archive"]["path"], self.build_cfg["_common"]["archive"]["filename"]) 66 | dst = os.path.join(self.tmp_build_dir, os.path.basename(src)) 67 | print(f"Copy archive file {src} {dst}") 68 | shutil.copyfile(src, dst) 69 | 70 | def populate_templates(self): 71 | title("Populate build templates") 72 | 73 | for tmpl in self.build_cfg["templates"]: 74 | print(tmpl) 75 | file_loader = FileSystemLoader(self.tmp_build_dir) 76 | env = Environment(loader=file_loader) 77 | template = env.get_template(tmpl["src"]) 78 | with open(os.path.join(self.tmp_build_dir, tmpl["src"]), "w") as template_file: 79 | template_file.write(template.render(self.build_cfg)) 80 | 81 | def build_package(self): 82 | title("Build package") 83 | cmd = [self.build_cfg["pkg_build"]["bin"]] 84 | cmd += self.build_cfg["pkg_build"]["args"] 85 | run(cmd, cwd=self.tmp_build_dir, check=True) 86 | 87 | def retrieve_assets(self): 88 | title("Retrieve assets") 89 | 90 | def teardown_environment(self): 91 | title("Clean build environment") 92 | 93 | def run(self): 94 | """ 95 | The sequence for the build process 96 | """ 97 | self.setup_environment() 98 | self.copy_archives() 99 | self.populate_templates() 100 | self.build_package() 101 | self.retrieve_assets() 102 | self.teardown_environment() 103 | 104 | class Rocky8(BuildTarget): 105 | def setup_environment(self): 106 | super().setup_environment() 107 | # rpmdev-setuptree creates the RPM build directory in the home env directory, so a symlink 108 | # is set to the package build directory and reverted in teardown_environment. 109 | build_rpm_path = pathlib.Path(self.tmp_build_dir, "rpmbuild") 110 | build_rpm_path.mkdir() 111 | self.build_cfg["_runtime"]["buildroot"] = build_rpm_path.resolve() 112 | 113 | home_rpm_path = pathlib.Path(pathlib.Path().home(), "rpmbuild") 114 | if home_rpm_path.is_symlink(): 115 | print("Unlink existing symlink") 116 | home_rpm_path.unlink() 117 | else: 118 | if home_rpm_path.exists(): 119 | raise EnvironmentError(f"{home_rpm_path} exists and is not a symlink. Refusing to alter filesystem state that wasn't created by the build script.") 120 | print(f"Creating symlink from {home_rpm_path} to {build_rpm_path.resolve()}") 121 | home_rpm_path.symlink_to(build_rpm_path.resolve()) 122 | 123 | print("Setting up RPM build tree") 124 | run(["rpmdev-setuptree"], cwd=self.tmp_build_dir, check=True) 125 | 126 | def copy_archives(self): 127 | # Rocky needs the archive in a different location compared to Ubuntu and Debian so it's overridden here. 128 | src = os.path.join(self.build_cfg["_common"]["archive"]["path"], self.build_cfg["_common"]["archive"]["filename"]) 129 | dst = os.path.join(self.tmp_build_dir, "rpmbuild", "SOURCES", os.path.basename(src)) 130 | print(f"Copy archive file {src} {dst}") 131 | shutil.copyfile(src, dst) 132 | 133 | def build_package(self): 134 | super().build_package() 135 | 136 | def teardown_environment(self): 137 | super().teardown_environment() 138 | print("Removing symlink") 139 | home_rpm_path = pathlib.Path(pathlib.Path().home(), "rpmbuild") 140 | home_rpm_path.unlink() 141 | 142 | 143 | class Rocky9(Rocky8): 144 | """ 145 | Rocky 9.0 steps _might_ be the same as Rocky8. 146 | """ 147 | pass 148 | 149 | class Ubuntu1804(BuildTarget): 150 | def __init__(self, build_cfg, distro): 151 | super().__init__(build_cfg, distro) 152 | 153 | def setup_environment(self): 154 | super().setup_environment() 155 | 156 | def copy_archives(self): 157 | super().copy_archives() 158 | 159 | def populate_templates(self): 160 | super().populate_templates() 161 | # Parse changelog for errors 162 | run(["dpkg-parsechangelog"], cwd=self.tmp_build_dir, check=True) 163 | 164 | def build_package(self): 165 | super().build_package() 166 | 167 | def retrieve_assets(self): 168 | super().retrieve_assets() 169 | 170 | def teardown_environment(self): 171 | super().teardown_environment() 172 | 173 | class Ubuntu2004(Ubuntu1804): 174 | """ 175 | Ubuntu 18.04 steps are sufficient to build for 20.04 176 | """ 177 | pass 178 | 179 | class Ubuntu2204(Ubuntu1804): 180 | """ 181 | Ubuntu 18.04 steps are sufficient to build for 22.04 182 | """ 183 | pass 184 | 185 | class Debian10(BuildTarget): 186 | def __init__(self, build_cfg, distro): 187 | super().__init__(build_cfg, distro) 188 | 189 | def setup_environment(self): 190 | super().setup_environment() 191 | 192 | def copy_archives(self): 193 | super().copy_archives() 194 | 195 | def populate_templates(self): 196 | super().populate_templates() 197 | 198 | # Parse changelog for errors 199 | run(["dpkg-parsechangelog"], cwd=self.tmp_build_dir, check=True) 200 | 201 | def build_package(self): 202 | super().build_package() 203 | cmd = [self.build_cfg["pkg_build"]["bin"]] 204 | cmd += self.build_cfg["pkg_build"]["args"] 205 | run(cmd, cwd=self.tmp_build_dir, check=True) 206 | 207 | def retrieve_assets(self): 208 | super().retrieve_assets() 209 | 210 | def teardown_environment(self): 211 | super().teardown_environment() 212 | 213 | class Debian11(Debian10): 214 | """ 215 | Build for Debian 11 216 | """ 217 | pass 218 | 219 | def load_config(cfg_file="build.cfg"): 220 | with open(cfg_file, "r") as f: 221 | return json.load(f) 222 | 223 | 224 | def temp_filename(): 225 | return "tmp{}".format( 226 | "".join(random.choices(string.ascii_lowercase + string.ascii_uppercase, k=10)) 227 | ) 228 | 229 | 230 | def release_info(release_files): 231 | """ 232 | release_files: A list of filenames to attempt to process for rel information. 233 | Returns are dictionary of key/value pairs in lower case. 234 | """ 235 | 236 | def rel_to_dict(text): 237 | rel = {} 238 | for l in shlex.split(text): 239 | if len(l): 240 | k, v = l.lower().split("=", 1) 241 | rel[k] = v 242 | return rel 243 | 244 | # Find rel release 245 | for release_file in release_files: 246 | if os.path.isfile(release_file): 247 | with open(release_file, "r") as f: 248 | rel = f.read() 249 | break 250 | else: 251 | raise "Failed to find rel release information." 252 | 253 | return rel_to_dict(rel) 254 | 255 | rel = release_info(release_files) 256 | # Strip the minor version since the script is designed to work on major versions. 257 | distro = "{}{}".format(rel["id"], rel["version_id"].split(".", 1)[0]) 258 | build_cfg = load_config() 259 | 260 | targets = { 261 | "ubuntu18": Ubuntu1804, 262 | "ubuntu20": Ubuntu2004, 263 | "ubuntu22": Ubuntu2204, 264 | "debian10": Debian10, 265 | "debian11": Debian11, 266 | "rocky8": Rocky8, 267 | "rocky9": Rocky9, 268 | } 269 | 270 | if distro not in build_cfg: 271 | print(f"{distro} missing configuration. Release information for host is:") 272 | for k, v in rel.items(): 273 | print(f"\t{k}: {v}") 274 | sys.exit(1) 275 | 276 | build_type = targets.get(distro) 277 | 278 | if not build_type: 279 | print(f"Build target {distro} is unsupported.") 280 | sys.exit(2) 281 | 282 | builder = build_type(build_cfg, distro) 283 | builder.run() 284 | -------------------------------------------------------------------------------- /contrib/packaging/build.cfg: -------------------------------------------------------------------------------- 1 | { 2 | "_common": { 3 | "build_dir": "pkg-build", 4 | "version": "2.2.0", 5 | "pkg_version": "3", 6 | "archive": { 7 | "path": "/opt", 8 | "filename": "err-stackstorm_{version}_{distro}_x86_64.tar.gz" 9 | }, 10 | "maintainer": { 11 | "name": "Carlos", 12 | "email": "nzlosh@yhaoo.com" 13 | } 14 | }, 15 | "rocky8": { 16 | "build_deps": [ 17 | "gcc", 18 | "rpm-build", 19 | "rpm-devel", 20 | "rpmlint", 21 | "make", 22 | "python38", 23 | "bash", 24 | "coreutils", 25 | "diffutils", 26 | "patch", 27 | "rpmdevtools" 28 | ], 29 | "runtime_deps": [ 30 | "python38" 31 | ], 32 | "pkg_build": { 33 | "bin": "rpmbuild", 34 | "args": ["-ba", "err-stackstorm.spec"] 35 | }, 36 | "pkg_bin": "dnf", 37 | "pybin": "python3.8", 38 | "template_dir": "templates/redhat", 39 | "templates": [ 40 | {"src": "err-stackstorm.spec"} 41 | ], 42 | "build_assets": [] 43 | }, 44 | "rocky9": { 45 | "build_deps": [ 46 | "gcc", 47 | "rpm-build", 48 | "rpm-devel", 49 | "rpmlint", 50 | "make", 51 | "python39", 52 | "bash", 53 | "coreutils", 54 | "diffutils", 55 | "patch", 56 | "rpmdevtools" 57 | ], 58 | "runtime_deps": [ 59 | "python39" 60 | ], 61 | "pkg_build": { 62 | "bin": "rpmbuild", 63 | "args": ["-ba", "err-stackstorm.spec"] 64 | }, 65 | "pkg_bin": "dnf", 66 | "pybin": "python3.9", 67 | "template_dir": "templates/redhat", 68 | "templates": [ 69 | {"src": "err-stackstorm.spec"} 70 | ], 71 | "build_assets": [] 72 | }, 73 | "debian10": { 74 | "build_deps": [ 75 | "build-essential", 76 | "devscripts", 77 | "debhelper", 78 | "python3.7-dev" 79 | ], 80 | "runtime_deps": [ 81 | "python3.7-minimal", 82 | "python3.7-venv" 83 | ], 84 | "pkg_build": { 85 | "bin": "fakeroot", 86 | "args": ["dpkg-buildpackage", "-us", "-uc"] 87 | }, 88 | "pkg_bin": "apt", 89 | "pybin": "python3.7", 90 | "template_dir": "templates/ubuntu", 91 | "templates": [ 92 | {"src": "debian/changelog"}, 93 | {"src": "debian/control"}, 94 | {"src": "debian/rules"}, 95 | {"src": "debian/watch"} 96 | ], 97 | "build_assets": [] 98 | }, 99 | "debian11": { 100 | "build_deps": [ 101 | "build-essential", 102 | "devscripts", 103 | "debhelper", 104 | "python3.9-dev" 105 | ], 106 | "runtime_deps": [ 107 | "python3.9-minimal", 108 | "python3.9-venv" 109 | ], 110 | "pkg_build": { 111 | "bin": "fakeroot", 112 | "args": ["dpkg-buildpackage", "-us", "-uc"] 113 | }, 114 | "pkg_bin": "apt", 115 | "pybin": "python3.9", 116 | "template_dir": "templates/ubuntu", 117 | "templates": [ 118 | {"src": "debian/changelog"}, 119 | {"src": "debian/control"}, 120 | {"src": "debian/rules"}, 121 | {"src": "debian/watch"} 122 | ], 123 | "build_assets": [] 124 | }, 125 | "ubuntu18": { 126 | "build_deps": [ 127 | "build-essential", 128 | "devscripts", 129 | "debhelper", 130 | "python3.7-dev" 131 | ], 132 | "runtime_deps": [ 133 | "python3.7", 134 | "python3.7-venv" 135 | ], 136 | "pkg_build": { 137 | "bin": "fakeroot", 138 | "args": ["dpkg-buildpackage", "-us", "-uc"] 139 | }, 140 | "pkg_bin": "apt", 141 | "pybin": "python3.7", 142 | "template_dir": "templates/ubuntu", 143 | "templates": [ 144 | {"src": "debian/changelog"}, 145 | {"src": "debian/control"}, 146 | {"src": "debian/rules"}, 147 | {"src": "debian/watch"} 148 | ], 149 | "build_assets": [] 150 | }, 151 | "ubuntu20": { 152 | "build_deps": [ 153 | "build-essential", 154 | "devscripts", 155 | "debhelper", 156 | "python3.8-dev" 157 | ], 158 | "runtime_deps": [ 159 | "python3.8", 160 | "python3.8-venv" 161 | ], 162 | "pkg_build": { 163 | "bin": "fakeroot", 164 | "args": ["dpkg-buildpackage", "-us", "-uc"] 165 | }, 166 | "pkg_bin": "apt", 167 | "pybin": "python3.8", 168 | "template_dir": "templates/ubuntu", 169 | "templates": [ 170 | {"src": "debian/changelog"}, 171 | {"src": "debian/control"}, 172 | {"src": "debian/rules"}, 173 | {"src": "debian/watch"} 174 | ], 175 | "build_assets": [] 176 | }, 177 | "ubuntu22": { 178 | "build_deps": [ 179 | "build-essential", 180 | "devscripts", 181 | "debhelper", 182 | "python3.10-dev" 183 | ], 184 | "runtime_deps": [ 185 | "python3.10", 186 | "python3.10-venv" 187 | ], 188 | "pkg_build": { 189 | "bin": "fakeroot", 190 | "args": ["dpkg-buildpackage", "-us", "-uc"] 191 | }, 192 | "pkg_bin": "apt", 193 | "pybin": "python3.8", 194 | "template_dir": "templates/ubuntu", 195 | "templates": [ 196 | {"src": "debian/changelog"}, 197 | {"src": "debian/control"}, 198 | {"src": "debian/rules"}, 199 | {"src": "debian/watch"} 200 | ], 201 | "build_assets": [] 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /contrib/packaging/build_archive: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eou pipefail +x 4 | 5 | ERR_STACKSTORM_VERSION=2.2.0 6 | 7 | # Package contains the following software projects and their respective licence. 8 | # 9 | # errbot GPL3 10 | # err-backend-discord GPL3 11 | # err-backend-slackv3 GPL3 12 | # err-backend-botframework MIT License 13 | # err-backend-mattermost GPL3 14 | # 15 | # err-stackstorm Apache 2.0 16 | # 17 | # The build process is the following: 18 | # 1. Acquire the source code and install build time dependencies 19 | # 2. Create virtualenv and install dependencies. 20 | # 3. Prune virtualenv of git metadata and python compiled bytecode. 21 | # 4. Create tar.gz snapshot of virtualenv for OS/architecture 22 | # 5. Run package build processusing latest tar.gz 23 | # 6. Update repository with package. 24 | 25 | # References 26 | # 27 | # https://gist.github.com/fernandoaleman/1377211/d78d13bd8f134e7d9b9bc3da5895c859d7cbf294 28 | # https://gist.githubusercontent.com/fernandoaleman/1377169/raw/3e841ca1a887dd21f3fcb35a3e74b0cc2fc4977b/create-repo-metadata.sh 29 | function title 30 | { 31 | echo -e "\033[38;5;206;48;5;57m${1}\033[0m" 32 | } 33 | function rocky8_install 34 | { 35 | dnf install -y "${PKGS[@]}" 36 | } 37 | function rocky9_install 38 | { 39 | dnf install -y "${PKGS[@]}" 40 | } 41 | function ubuntu18_install 42 | { 43 | apt install "${PKGS[@]}" 44 | } 45 | function ubuntu20_install 46 | { 47 | apt install "${PKGS[@]}" 48 | } 49 | function ubuntu22_install 50 | { 51 | apt install "${PKGS[@]}" 52 | } 53 | function debian10_install 54 | { 55 | apt install "${PKGS[@]}" 56 | } 57 | function debian11_install 58 | { 59 | apt install "${PKGS[@]}" 60 | } 61 | 62 | function install_system_requirements 63 | { 64 | title "INSTALL SYSTEM REQUIREMENTS" 65 | "${DISTRO_COMBO}_install" 66 | } 67 | 68 | function create_virtual_environment 69 | { 70 | title "CREATE VIRTUAL ENVIRONMENT" 71 | "${PYBIN}" -m venv "${ROOT}" 72 | cd "${ROOT}" 73 | mkdir ./{plugins,backends,data} 74 | } 75 | 76 | function fetch_archives 77 | { 78 | title "DOWNLOAD ARCHIVES" 79 | export BACKENDS=("err-backend-discord" "err-backend-slackv3" "err-backend-gitter" "err-backend-mattermost" "err-backend-botframework") 80 | 81 | mkdir "${BUILD_DIR}/backends" 82 | cd "${BUILD_DIR}" 83 | wget "https://github.com/errbotio/err-backend-discord/archive/refs/tags/v4.0.0.tar.gz" -O "backends/err-backend-discord-v4.0.0.tar.gz" 84 | wget "https://github.com/errbotio/err-backend-slackv3/archive/refs/tags/v0.2.1.tar.gz" -O "backends/err-backend-slackv3-v0.2.1.tar.gz" 85 | wget "https://github.com/errbotio/err-backend-mattermost/archive/refs/tags/3.0.0.tar.gz" -O "backends/err-backend-mattermost-v3.0.0.tar.gz" 86 | wget "https://github.com/nzlosh/err-backend-gitter/archive/refs/tags/v0.1.0.tar.gz" -O "backends/err-backend-gitter-v0.1.0.tar.gz" 87 | wget "https://github.com/nzlosh/err-backend-botframework/archive/refs/tags/v0.1.0.tar.gz" -O "backends/err-backend-botframework-v0.1.0.tar.gz" 88 | 89 | mkdir "${BUILD_DIR}/plugins" 90 | export PLUGINS=("err-stackstorm") 91 | wget "https://github.com/nzlosh/err-stackstorm/archive/refs/tags/v${ERR_STACKSTORM_VERSION}.tar.gz" -O "plugins/err-stackstorm-v${ERR_STACKSTORM_VERSION}.tar.gz" 92 | } 93 | 94 | function install_extensions 95 | { 96 | title "INSTALL ERRBOT EXTENSIONS" 97 | for location in backends plugins 98 | do 99 | # extract 100 | for targz in "${BUILD_DIR}/${location}"/*.tar.gz 101 | do 102 | tar xf "$targz" -C "${ROOT}/${location}" 103 | done 104 | # install dependencies 105 | for proj in "${ROOT}/${location}"/* 106 | do 107 | # Install from pyproject 108 | test -f "${proj}/pyproject.toml" && pip install -e "${proj}" 109 | # Install from requirements 110 | test -f "${proj}/requirements.txt" && pip install -r "${proj}/requirements.txt" 111 | done 112 | done 113 | 114 | for location in "${ROOT}/backends" 115 | do 116 | cd "${location}" 117 | for backend in "${BACKENDS[@]}" 118 | do 119 | # Move archive directory names to correct errbot names. 120 | mv -f ./*"${backend}"* "$backend" 121 | done 122 | done 123 | 124 | for location in "$ROOT/plugins" 125 | do 126 | cd "$location" 127 | for plugin in "${PLUGINS[@]}" 128 | do 129 | mv -f ./*"${plugin}"* "$plugin" 130 | done 131 | done 132 | } 133 | 134 | function prune_installation 135 | { 136 | title "PRUNE INSTALLATION" 137 | # Remove python cache files and hidden files. 138 | find "$ROOT/venv" -iname '*.pyc' -delete 139 | # Remove unrequired directories 140 | for dir in '__pycache__' ".github" "docs" "tests" 141 | do 142 | find "${ROOT}/venv" -iname "$dir" -type d -exec rm -rf "{}" \; 143 | done 144 | for f in '.*' 'manifest.in' 145 | do 146 | # Remove hidden project files 147 | find "${ROOT}/venv" -iname '.*' -delete 148 | done 149 | # Remove non-core code from err-stackstorm 150 | for excess in contrib docs tests 151 | do 152 | rm -rf "$ROOT/plugins/err-stackstorm/${excess}" 153 | done 154 | # Remove temp build dir 155 | rm -rf "$BUILD_DIR" 156 | } 157 | 158 | 159 | 160 | function install_errbot 161 | { 162 | title "INSTALL ERRBOT ($($ROOT/venv/bin/pip --version))" 163 | source "${ROOT}/venv/bin/activate" 164 | 165 | pip install --upgrade pip 166 | # Use tmp dir to download and build errbot/plugins. 167 | export BUILD_DIR=$(mktemp -d "$ROOT"/build.XXXXXX) 168 | cd "$BUILD_DIR" 169 | ERRBOT_VERSION="6.2.0" 170 | wget "https://github.com/errbotio/errbot/archive/refs/tags/${ERRBOT_VERSION}.tar.gz" -O errbot-v${ERRBOT_VERSION}.tar.gz 171 | tar xf errbot-v${ERRBOT_VERSION}.tar.gz 172 | cd errbot-${ERRBOT_VERSION} 173 | pip install -e .[IRC,XMPP,telegram] 174 | fetch_archives 175 | install_extensions 176 | prune_installation 177 | } 178 | 179 | 180 | function build_archive 181 | { 182 | install_system_requirements 183 | create_virtual_environment 184 | install_errbot 185 | tar czf "/opt/err-stackstorm_${ERR_STACKSTORM_VERSION}_${DISTRO_COMBO}_x86_64.tar.gz" "$ROOT" 186 | } 187 | 188 | export DISTRO=$(source /etc/os-release; echo $ID) 189 | export DISTRO_VERSION=$(source /etc/os-release; echo $VERSION_ID) 190 | # Strip the minor version since the script is designed to work on major versions. 191 | export DISTRO_COMBO="${DISTRO}${DISTRO_VERSION%.*}" 192 | export ROOT="/opt/errbot" 193 | 194 | case "${DISTRO_COMBO}" in 195 | rocky8) 196 | export PKGS=(python38 virtualenv python38-devel git wget) 197 | export PYBIN=/usr/bin/python3.8 198 | ;; 199 | rocky9) 200 | export PKGS=(python39 python3-devel tar gcc git wget) 201 | export PYBIN=/usr/bin/python3.9 202 | ;; 203 | ubuntu20) 204 | export PKGS=(python3.8-minimal git) 205 | export PYBIN=/usr/bin/python3.8 206 | ;; 207 | ubuntu22) 208 | export PKGS=(python3.10-minimal git) 209 | export PYBIN=/usr/bin/python3.10 210 | ;; 211 | debian11) 212 | export PKGS=(python3.9-minimal git wget) 213 | export PYBIN=/usr/bin/python3.9 214 | ;; 215 | *) 216 | echo "Distribution ${DISTRO} version ${DISTRO_VERSION} isn't supported." 217 | exit 1 218 | ;; 219 | esac 220 | 221 | build_archive 222 | -------------------------------------------------------------------------------- /contrib/packaging/requirements.txt: -------------------------------------------------------------------------------- 1 | jinja2 2 | -------------------------------------------------------------------------------- /contrib/packaging/templates/redhat/err-stackstorm.spec: -------------------------------------------------------------------------------- 1 | Name: err-stackstorm 2 | Version: {{ _common.version }} 3 | Release: {{ _common.pkg_version }} 4 | Summary: Bringing StackStorm ChatOps to errbot. 5 | License: GPL3, MIT and Apache 2.0 6 | Packager: {{ _common.maintainer.name }} <{{ _common.maintainer.email }}> 7 | BuildRoot: {{ _runtime.buildroot }} 8 | 9 | #URL 10 | Source0: {{ _common.archive.filename }} 11 | BuildArch: x86_64 12 | 13 | #BuildRequires: 14 | #Requires: 15 | 16 | 17 | 18 | %description 19 | A python virtual environment with the following software installed: 20 | - errbot GPL3 21 | 22 | - err-backend-discord GPL3 23 | - err-backend-slackv3 GPL3 24 | - err-backend-botframework MIT Licence 25 | - err-backend-mattermost GPL3 26 | 27 | - err-stackstorm Apache 2.0 28 | 29 | %prep 30 | echo No prep 31 | 32 | %build 33 | echo Creating archive extraction directory 34 | mkdir -p %{buildroot} 35 | 36 | %install 37 | echo Extract archive into rpm buildroot. 38 | tar xvf /root/rpmbuild/SOURCES/{{ _common.archive.filename }} --directory %{buildroot} 39 | 40 | %files 41 | /opt/errbot 42 | 43 | %changelog 44 | * {{ _runtime.dyslexic_datetime }} {{ _common.maintainer.name }} <{{ _common.maintainer.email }}> 45 | - Virtual environment of errbot and err-stackstorm. 46 | -------------------------------------------------------------------------------- /contrib/packaging/templates/ubuntu/debian/changelog: -------------------------------------------------------------------------------- 1 | err-stackstorm ({{ _common.version }}+{{ _common.pkg_version }}) UNRELEASED; urgency=low 2 | 3 | * Virtual environment of errbot and err-stackstorm. 4 | 5 | -- {{ _common.maintainer.name }} <{{ _common.maintainer.email }}> {{ _runtime.rfc2822_datetime }} 6 | 7 | -------------------------------------------------------------------------------- /contrib/packaging/templates/ubuntu/debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /contrib/packaging/templates/ubuntu/debian/control: -------------------------------------------------------------------------------- 1 | Source: err-stackstorm 2 | Section: unknown 3 | Priority: extra 4 | Maintainer: Carlos 5 | Build-Depends: debhelper (>=10) 6 | Standards-Version: 3.9.4 7 | Homepage: https://err-stackstorm.readthedocs.io/en/latest/ 8 | Vcs-Git: git://github.com/nzlosh/err-stackstorm.git 9 | 10 | Package: err-stackstorm 11 | Architecture: amd64 12 | Depends: ${misc:Depends}, {{ ", ".join(runtime_deps) }} 13 | Description: err-stackstorm brings StackStorm ChatOps to Errbot. 14 | -------------------------------------------------------------------------------- /contrib/packaging/templates/ubuntu/debian/err-stackstorm.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Errbot with err-stackstorm 3 | After=network.target 4 | 5 | [Service] 6 | User=errbot 7 | Environment="CONFIGFILE=/opt/errbot/config.py" 8 | ExecStart=/opt/errbot/bin/python /opt/errbot/bin/errbot --config $CONFIGFILE 9 | ExecStop=/bin/kill -SIGINT $MAINPID 10 | Restart=on-failure 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /contrib/packaging/templates/ubuntu/debian/patches/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nzlosh/err-stackstorm/32fa23a5821bcbccd05c2e65e89b1aaec9402c9e/contrib/packaging/templates/ubuntu/debian/patches/.placeholder -------------------------------------------------------------------------------- /contrib/packaging/templates/ubuntu/debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | VERB="$1" 6 | 7 | E_BAD_VERB=1 8 | E_BAD_VERSION=2 9 | E_BAD_ARGS=3 10 | 11 | 12 | function check_version 13 | { 14 | VERSION="$1" 15 | if [[ -z "$VERSION" ]]; then 16 | echo "Aborting! $0 $VERB called without providing a package version." 17 | exit $E_BAD_VERSION 18 | fi 19 | } 20 | 21 | function create_system_account 22 | { 23 | /usr/bin/getent passwd errbot >/dev/null || export RET=$? 24 | test -z "$RET" || export RET=0 25 | 26 | if [[ $RET -eq 2 ]]; then 27 | echo "Creating errbot system user." 28 | adduser --system --home /opt/errbot --shell /bin/false errbot --group 29 | elif [[ $RET -ne 0 ]]; then 30 | echo "Error getting entity information for errbot." > /dev/stderr 31 | exit $RET 32 | fi 33 | 34 | ERRBOT_PATH="/opt/errbot" 35 | if [[ -d "$ERRBOT_PATH" ]]; then 36 | echo "Setting ownership for $ERRBOT_PATH" 37 | chown -R errbot:errbot "$ERRBOT_PATH" 38 | fi 39 | } 40 | 41 | 42 | case "$VERB" in 43 | configure) 44 | VERSION="$2" 45 | check_version "$VERSION" 46 | echo "$0 $VERB most-recently-configured-version: $VERSION" 47 | create_system_account 48 | ;; 49 | abort-upgrade) 50 | VERSION="$2" 51 | check_version "$VERSION" 52 | echo "$0 $VERB new-version: ${VERSION}" 53 | ;; 54 | abort-remove) 55 | if [[ -z "$2" ]]; then 56 | echo "$0 $VERB" 57 | elif [[ "$2" = "in-favour" ]]; then 58 | PACKAGE="$3" 59 | VERSION="$4" 60 | check_version "$VERSION" 61 | echo "$0 $VERB in-favour package:$PACKAGE new-version:$VERSION" 62 | else 63 | echo "$0 $VERB does support argument '$2'" 64 | exit $E_BAD_ARGS 65 | fi 66 | ;; 67 | abort-deconfigure) 68 | if [[ "$2" = "in-favour" ]]; then 69 | PACKAGE="$3" 70 | VERSION="$4" 71 | check_version "$VERSION" 72 | echo "$0 $VERB in-favour failed-install-package: $PACKAGE version: $VERSION" 73 | else 74 | echo "$0 $VERB does support argument '$2'" 75 | exit $E_BAD_ARGS 76 | fi 77 | ;; 78 | *) 79 | echo "$0 called with unexpected package action '$VERB'" 80 | exit $E_BAD_VERB 81 | ;; 82 | esac 83 | -------------------------------------------------------------------------------- /contrib/packaging/templates/ubuntu/debian/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | VERB="$1" 6 | VERSION="$2" 7 | 8 | E_BAD_VERB=1 9 | E_BAD_VERSION=2 10 | E_BAD_ARGS=3 11 | 12 | function check_version 13 | { 14 | VERSION="$1" 15 | if [[ -z "$VERSION" ]]; then 16 | echo "Aborting! $0 $VERB called without providing a package version." 17 | exit $E_BAD_VERSION 18 | fi 19 | } 20 | 21 | case "$VERB" in 22 | remove) 23 | echo "$0 $VERB" 24 | ;; 25 | purge) 26 | echo "$0 $VERB" 27 | ;; 28 | upgrade) 29 | VERSION="$2" 30 | check_version "$VERSION" 31 | echo "$0 $VERB new-version: $VERSION" 32 | ;; 33 | disappear) 34 | PACKAGE="$2" 35 | VERSION="$3" 36 | check_version "$VERSION" 37 | echo "$0 $VERB overwriter: $PACKAGE, overwriter-version: $VERSION" 38 | ;; 39 | failed-upgrade) 40 | VERSION="$2" 41 | check_version "$VERSION" 42 | echo "$0 $VERB old-version: $VERSION" 43 | ;; 44 | abort-install) 45 | VERSION="$2" 46 | if [[ -z "$VERSION" ]]; then 47 | echo "$0 $VERB" 48 | else 49 | echo "$0 $VERB old-version: $VERSION" 50 | fi 51 | ;; 52 | abort-upgrade) 53 | VERSION="$2" 54 | check_version "$VERSION" 55 | echo "$0 $VERB old-version: $VERSION" 56 | ;; 57 | *) 58 | echo "$0 called with unexpected package action '$VERB'" 59 | exit $E_BAD_VERB 60 | ;; 61 | esac 62 | -------------------------------------------------------------------------------- /contrib/packaging/templates/ubuntu/debian/preinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | VERB="$1" 6 | VERSION="$2" 7 | 8 | E_BAD_VERB=1 9 | E_BAD_VERSION=2 10 | E_BAD_ARGS=3 11 | 12 | function check_version 13 | { 14 | VERSION="$1" 15 | if [[ -z "$VERSION" ]]; then 16 | echo "Aborting! $0 $VERB called without providing a package version." 17 | exit $E_BAD_VERSION 18 | fi 19 | } 20 | 21 | case "$VERB" in 22 | install) 23 | if [[ -z "$VERSION" ]]; then 24 | echo "$0 $VERB" 25 | else 26 | echo "$0 $VERB old-version: $VERSION" 27 | fi 28 | ;; 29 | upgrade) 30 | check_version "$VERSION" 31 | echo "$0 $VERB old-version: $VERSION" 32 | ;; 33 | abort-upgrade) 34 | check_version "$VERSION" 35 | echo "$0 $VERB old-version: $VERSION" 36 | ;; 37 | *) 38 | echo "$0 called with unexpected package action '$VERB'" 39 | exit $E_BAD_VERB 40 | ;; 41 | esac 42 | -------------------------------------------------------------------------------- /contrib/packaging/templates/ubuntu/debian/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | VERB="$1" 6 | 7 | E_BAD_VERB=1 8 | E_BAD_VERSION=2 9 | E_BAD_ARGS=3 10 | 11 | function check_version 12 | { 13 | VERSION="$1" 14 | if [[ -z "$VERSION" ]]; then 15 | echo "Aborting! $0 $VERB called without providing a package version." 16 | exit $E_BAD_VERSION 17 | fi 18 | } 19 | 20 | case "$VERB" in 21 | upgrade) 22 | VERSION="$2" 23 | check_version "$VERSION" 24 | echo "$0 $VERB new-version: $VERSION" 25 | ;; 26 | remove) 27 | if [[ "$2" = "in-favour" ]]; then 28 | PACKAGE="$3" 29 | VERSION="$4" 30 | check_version "$VERSION" 31 | echo "$0 $VERB in-favour package-being-installed: $PACKAGE, new-version: $VERSION" 32 | else 33 | echo "$0 $VERB" 34 | fi 35 | ;; 36 | deconfigure) 37 | if [[ "$2" = "in-favour" ]]; then 38 | PACKAGE="$3" 39 | VERSION="$4" 40 | check_version "$VERSION" 41 | echo "$0 $VERB in favour package-being-installed:$PACKAGE version: $VERSION" 42 | else 43 | echo "$0 $VERB called without correct arguments '$2'" 44 | exit $E_BAD_ARGS 45 | fi 46 | ;; 47 | failed-upgrade) 48 | VERSION="$2" 49 | check_version "$VERSION" 50 | echo "$0 $VERB old-version: $VERSION" 51 | ;; 52 | *) 53 | echo "$0 called with unexpected package action '$VERB'" 54 | exit $E_BAD_VERB 55 | ;; 56 | esac 57 | -------------------------------------------------------------------------------- /contrib/packaging/templates/ubuntu/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | #export DH_VERBOSE = 1 3 | 4 | DESTDIR=$(CURDIR)/debian/err-stackstorm 5 | 6 | %: 7 | dh $@ 8 | 9 | override_dh_installdeb: 10 | dh_installdeb 11 | echo BUILD DIRECTORY IS $(DESTDIR) 12 | mkdir -p $(DESTDIR) 13 | echo Extracting {{ _common.archive.filename }} to $(DESTDIR) 14 | tar xf $(CURDIR)/{{ _common.archive.filename }} --directory $(DESTDIR) 15 | -------------------------------------------------------------------------------- /contrib/packaging/templates/ubuntu/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /contrib/packaging/templates/ubuntu/debian/watch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nzlosh/err-stackstorm/32fa23a5821bcbccd05c2e65e89b1aaec9402c9e/contrib/packaging/templates/ubuntu/debian/watch -------------------------------------------------------------------------------- /contrib/stackstorm-chatops/rules/notify_errbot.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "notify-errbot" 3 | pack: "chatops" 4 | enabled: true 5 | description: "Notification rule to send results of action executions to stream for chatops" 6 | trigger: 7 | type: "core.st2.generic.notifytrigger" 8 | criteria: 9 | trigger.route: 10 | pattern: "errbot" 11 | type: "equals" 12 | action: 13 | ref: chatops.post_result 14 | parameters: 15 | channel: "{{trigger.data.source_channel}}" 16 | user: "{{trigger.data.user}}" 17 | execution_id: "{{trigger.execution_id}}" 18 | -------------------------------------------------------------------------------- /contrib/systemd/errbot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Errbot 3 | After=network.target 4 | 5 | [Service] 6 | User=errbot 7 | Environment="CONFIGFILE=/data/errbot/etc/config.py" 8 | ExecStart=/opt/errbot/bin/python /opt/errbot/bin/errbot --config $CONFIGFILE 9 | ExecStop=/bin/kill -SIGINT $MAINPID 10 | Restart=on-failure 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/action_aliases.rst: -------------------------------------------------------------------------------- 1 | .. _action_aliases: 2 | 3 | **************** 4 | Action Aliases 5 | **************** 6 | 7 | .. note:: This section assumes you have little to no familiarity with StackStorm. If that's not the case, you may skip it. 8 | 9 | Overview 10 | --------- 11 | 12 | `Action Aliases `_ are a StackStorm feature that helps *exposing actions to the bot*. Their main purpose is to provide a *simple human readable* representation of actions, very useful in text-based interfaces, notable ChatOps. 13 | 14 | Generally speaking, you can expose any action to the bot. You can list them using `st2 action-alias list`. 15 | 16 | Creating a simple action alias 17 | ------------------------------- 18 | 19 | Let's create a simple action alias to demonstrate `err-stackstorm`. Aliases are deployed via packs so let's create one quickly:: 20 | 21 | mkdir -p //packs/errtest/aliases 22 | 23 | 24 | Create a file inside the ``aliases`` folder named ``run_remote.yaml`` and paste the yaml defined below: 25 | 26 | .. code-block:: yaml 27 | 28 | --- 29 | name: "local_shell_cmd" 30 | action_ref: "core.local" 31 | description: "Execute a command on a remote host via SSH." 32 | formats: 33 | - "run {{cmd}}" 34 | result: 35 | format: "operation completed {~} {{ execution.result }}" 36 | 37 | After that, ask StackStorm to reload its configuration:: 38 | 39 | st2ctl reload --register-all 40 | 41 | .. note:: Errbot will automatically refresh its action alias list when you add or remove aliases on StackStorm. Type ``!st2help`` to your bot on the chat to list available StackStorm commands. 42 | 43 | In this example ``local_shell_cmd`` is an alias for the ``core.local`` action. If you want to run a command against a remote host, you could have used the ``core.remote`` action. 44 | 45 | The supported format for the alias is specified in the formats field. A single alias can support multiple formats for the same action. The result will then be returned to `err-stackstorm` and Errbot will propagate that back to your chat backend. 46 | 47 | Usage 48 | ------ 49 | 50 | Once the alias is setup, talk to your bot and type:: 51 | 52 | !st2 run date 53 | 54 | The bot will answer your request using the ``result.format`` definition:: 55 | 56 | operation completed {~} Sun Jul 7 02:08:58 PDT 2019 57 | 58 | 59 | Formatting 60 | ---------- 61 | 62 | Jinja Template 63 | ============== 64 | 65 | .. seealso:: Don't forget to check St2 official documentation on `Action Aliases `_. 66 | 67 | Here's an example of an action-alias that runs a command on a list of remote hosts and outputs the results nicely formatted on **Slack**. 68 | 69 | 70 | .. code-block:: yaml 71 | 72 | --- 73 | name: "remote_shell_cmd" 74 | action_ref: "core.remote" 75 | description: "Execute a command on a remote host via SSH." 76 | formats: 77 | - "run {{cmd}} on {{hosts}}" 78 | result: 79 | format: | 80 | Ran command *{{execution.parameters.cmd}}* on *{{ execution.result | length }}* hosts. 81 | 82 | {% for host in execution.result %} 83 | \*Host\*: {{host}} {{ ":white_check_mark:" if execution.result[host].stdout else ":x:" }} 84 | \`\`\`{{ execution.result[host].stdout or execution.result[host].stderr or "No result"}}\`\`\` 85 | {% endfor %} 86 | 87 | The alias above will format the execution output per host once it gathers the results. The sheer amount backticks and escaping are due to particularities between Errbot and Slack - this may not work in another chat backend. 88 | 89 | This is how the bot will answer you on Slack: 90 | 91 | .. image:: images/remote_shell.jpg 92 | 93 | Slack Attachments 94 | ================= 95 | 96 | .. note:: Slack considers attachments as legacy formatting. Use block formatting whenever possible. Support for attachments in this form of dictionary may be removed from err-stackstorm in the future. 97 | 98 | Slack's Markdown can get you a long way, but there are some occasions a richer message format is preferable. 99 | 100 | Attachments were the first form of advanced message formatting provided by Slack. StackStorm ChatOps pack allows you to 101 | supply a `slack` key inside the `extra` parameter. The `slack` key can hold the set of attributes related to sending an attachment. 102 | Information on the available attachment fields can be found here. https://api.slack.com/reference/messaging/attachments#legacy_fields 103 | 104 | .. code-block:: bash 105 | st2 run chatops.post_message route=errbot_slack channel='<#CL8HNNTFY>' message='Attachments Test' extra=' 106 | { 107 | "slack": { 108 | "color": "#f48527", 109 | "pretext": "Hey , Ready for ChatOps?", 110 | "title": "SaltStack and ChatOps. Get started :rocket:", 111 | "title_link": "https://stackstorm.com/2015/07/29/getting-started-with-stackstorm-and-saltstack/", 112 | "author_name": "by Jurnell Cockhren, CTO and Founder of SophicWare", 113 | "author_link": "http://sophicware.com/", 114 | "author_icon": "https://stackstorm.com/wp/wp-content/uploads/2015/01/favicon.png", 115 | "image_url": "https://i.imgur.com/vOU2SC0.png", 116 | "fields": [{ 117 | "title": "Documentation", 118 | "value": "https://docs.stackstorm.com/chatops/", 119 | "short": true 120 | }] 121 | } 122 | }' 123 | 124 | 125 | 126 | Slack Blocks 127 | ============ 128 | 129 | Blocks have replaced Attachments as Slack's preferred method of advanced message formatting. Blocks allow interaction as well as formatting. 130 | Attachments can be used inside blocks to provide secondary information to the primary message but their display is not guaranteed by Slack. 131 | 132 | .. code-block:: bash 133 | st2 run chatops.post_message route=errbot_slack channel='<#CL8HNNTFY>' message='Blocks' extra='{ 134 | "slack": { 135 | "blocks": [{ 136 | "type": "section", 137 | "text": { 138 | "type": "plain_text", 139 | "text": "This is a plain text section block with jinja template interpolation {{ execution.id }}.", 140 | "emoji": true 141 | } 142 | }, { 143 | "type": "section", 144 | "text": { 145 | "type": "mrkdwn", 146 | "text": "This is a *section* block with an ~image~ text _formatting_." 147 | } 148 | }, { 149 | "type": "image", 150 | "title": { 151 | "type": "plain_text", 152 | "text": "Slack Errbot StackStorm SaltStack", 153 | "emoji": true 154 | }, 155 | "image_url": "https://i.imgur.com/vOU2SC0.png", 156 | "alt_text": "SESS" 157 | }] 158 | } 159 | }' 160 | 161 | 162 | .. important:: Advanced formatting may not be available to all chat backends since each backend requires specific code to translate St2 `extra` parameter. 163 | -------------------------------------------------------------------------------- /docs/authn.rst: -------------------------------------------------------------------------------- 1 | .. _authentication: 2 | 3 | ************************************************************************ 4 | Authentication 5 | ************************************************************************ 6 | 7 | .. contents:: :local: 8 | 9 | .. important:: Do not configure multiple authentication methods at the same time. 10 | 11 | Stand-alone 12 | ======================================================================== 13 | 14 | This is the default authentication method where `err-stackstorm` uses its own credentials for all calls to the StackStorm API. All action-aliases issued by chat service users execute the underlying workflows with `err-stackstorm` credentials. 15 | 16 | Any Role Based Access Control policies can only be applied to the bot user which gives limited control. 17 | 18 | Configuration 19 | ------------------------------------------------------------------------ 20 | 21 | An empty dictionary in the `standalone` key is all that is required to maintain err-stackstorm's default authentication method. 22 | 23 | .. code-block:: python 24 | 25 | "rbac_auth": { 26 | "standalone": {} 27 | } 28 | 29 | Client-Side 30 | ======================================================================== 31 | 32 | 33 | .. note:: This implementation is specific to err-stackstorm. 34 | 35 | `err-stackstorm` provides a way to associate the chat service user account with a StackStorm username/password or API token. Once a chat user is associated with their StackStorm credentials, any action-alias will be executed using the associated StackStorm credentials. 36 | 37 | This method allows for finer control as StackStorm's Role Based Access Control can be defined per-user. 38 | 39 | Chat users create a new authentication session with `err-stackstorm` by calling `session_start` with a secret word, it can be any thing the user wishes. A `UUID `_ is generated for the session and the chat user is invited to follow a URL to an authentication page hosted by Errbot. 40 | 41 | .. important:: For security reasons, the UUID is used only once and is consumed when the page is accessed. Any subsequent attempts to access the authentication page using the same link will result in an error. 42 | 43 | The login page must be protected by TLS encryption and ideally require an SSL client certificate. The login page should not be exposed directly to the internet, but have a reverse proxy such as nginx placed between it and any external service consumers. 44 | 45 | The user enters their StackStorm credentials via the login page which err-stackstorm will validate against the StackStorm API. If the credentials are valid, the user token or api key will be cached by err-stackstorm and the supplied credentials discarded. 46 | 47 | 48 | Configuration 49 | ------------------------------------------------------------------------ 50 | 51 | To configure the Client-Side authentication method, one needs to take steps to setup both Nginx and Errbot. Nginx is used to serve static web content for the authentication web page and Errbot serves as the API backend for authentication calls. 52 | 53 | 54 | NGINX 55 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 56 | 57 | .. note:: This example is provided as a guide only. You are expected to know and understand how to configure nginx for your environment. It is outside of the scope of this documentation to go over nginx's configuration and explain SSL certificates. 58 | 59 | .. important:: This example does not show how to secure access using SSL certificates. It is **highly recommended** to use SSL for production environments. 60 | 61 | To help understand the example below, the following conditions are assumed: 62 | 63 | * the nginx server is running *on the same host* as errbot. 64 | * the host's fully qualified domain name is ``my_host.my_fqdn``. 65 | * nginx listens to standard SSL-enabled port: 443. 66 | * Errbot's webserver listens on the ip address 127.0.0.1 TCP port 3141 without ssl enabled. 67 | * Errbot's plugins directory is /data/errbot/plugins 68 | * `err-stackstorm` is installed in /data/errbot/plugins/nzlosh/err-stackstorm. 69 | * The SSL certificate and private key used by nginx are called errbot.crt and errbot.key. 70 | 71 | .. code-block:: nginx 72 | 73 | server { 74 | listen my_host.my_fqdn:443 ssl; 75 | server_name my_host.my_fqdn; 76 | 77 | ssl on; 78 | 79 | ssl_certificate /etc/ssl/errbot.crt; 80 | ssl_certificate_key /etc/ssl/errbot.key; 81 | ssl_session_cache shared:SSL:10m; 82 | ssl_session_timeout 5m; 83 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 84 | ssl_ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:ECDHE-RSA-AES128-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256: 85 | ECDHE-RSA-AES128-GCM-SHA128:DHE-RSA-AES128-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA128: 86 | ECDHE-RSA-AES128-SHA384:ECDHE-RSA-AES128-SHA128:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA128: 87 | DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA: 88 | AES128-GCM-SHA384:AES128-GCM-SHA128:AES128-SHA128:AES128-SHA128:AES128-SHA:AES128-SHA:DES-CBC3-SHA:HIGH: 89 | !aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4; 90 | ssl_prefer_server_ciphers on; 91 | 92 | index index.html index.htm; 93 | 94 | access_log /var/log/nginx/ssl-errbot.access.log combined; 95 | error_log /var/log/nginx/ssl-errbot.error.log; 96 | 97 | add_header Front-End-Https on; 98 | add_header X-Content-Type-Options nosniff; 99 | 100 | location /login/ { 101 | proxy_pass http://127.0.0.1:3141$request_uri; 102 | proxy_read_timeout 90; 103 | proxy_connect_timeout 90; 104 | proxy_redirect off; 105 | 106 | proxy_set_header Host my_host.my_fqdn; 107 | proxy_set_header X-Real-IP $remote_addr; 108 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 109 | 110 | proxy_set_header Connection ''; 111 | chunked_transfer_encoding off; 112 | proxy_buffering off; 113 | proxy_cache off; 114 | proxy_set_header Host my_host.my_fqdn; 115 | } 116 | 117 | location / { 118 | root /data/errbot/plugins/nzlosh/err-stackstorm/html/; 119 | index index.html index.htm; 120 | } 121 | } 122 | 123 | After successfully setting up nginx, the client side authentication url would be ``https://my_host.my_fqdn:443/``. 124 | 125 | err-stackstorm configuration 126 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 127 | 128 | A url is required to correctly configure client-side authentication for ChatOps. This URL is Errbot's authentication endpoint that you have just set up. 129 | 130 | .. code-block:: python 131 | 132 | "rbac_auth": { 133 | "clientside": { 134 | "url": "https://:/" 135 | } 136 | }, 137 | 138 | Authenticating 139 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 140 | 141 | Once the client side authentication is setup, you should be able to trigger the authentication process with ``!session_start my_secret_word`` which will return a url to complete the login processes. This is how the page looks like: 142 | 143 | .. image:: images/authentication_screen.jpg 144 | -------------------------------------------------------------------------------- /docs/authz.rst: -------------------------------------------------------------------------------- 1 | .. _authorisation: 2 | 3 | *************** 4 | Authorisation 5 | *************** 6 | 7 | .. contents:: :local: 8 | 9 | 10 | Errbot Access Control List 11 | ========================== 12 | 13 | Errbot comes with native Access Control List support. It can be configured to constrain command execution by grouping ``command``, ``channel`` and ``user``. Glob patterns can be used in each field to provide flexibility in ACL definitions. 14 | 15 | As an example, a StackStorm instance has an automatic package upgrade workflow. Its progress can be viewed by executing the action alias: ``apu stats ``, which is defined as shown below:: 16 | 17 | | action_ref | st2_apu.apu_status | 18 | | formats | [ | 19 | | | { | 20 | | | "representation": [ | 21 | | | "apu status {{role}}" | 22 | | | ], | 23 | | | "display": "apu status " | 24 | | | } | 25 | | | ] | 26 | 27 | The Errbot ACL configuration below allows ``@user1`` to view the status of the upgrade, but *not to start/stop* the upgrade process, which are other action-aliases that are triggered with ``st2 apu ...``) 28 | 29 | .. code-block:: python 30 | 31 | ACL_SQUAD_INFRA = ["@admin1", "@admin2", "@admin3", "@admin4"] 32 | ACL_APU_USERS = ['@user1'] 33 | ACL_EVERYONE = ["*"] 34 | ACCESS_CONTROLS = { 35 | 'whoami': { 36 | 'allowrooms': ['@bot_user'], 37 | 'allowusers': ACL_EVERYONE 38 | }, 39 | 'st2 apu status*':{ 40 | 'allowrooms': ['#channel'], 41 | 'allowusers': ACL_SQUAD_INFRA + ACL_APU_USERS 42 | }, 43 | 'st2 apu*':{ 44 | 'allowrooms': ['#channel'], 45 | 'allowusers': ACL_SQUAD_INFRA 46 | }, 47 | } 48 | 49 | Getting the correct usernames to fill into ``allowusers`` or ``denyusers`` isn't always obvious. Use errbot's ``!whoami`` command to get the correct username for use within ACL definitions. The `nick` value is what should be used in the configuration in the case of Slack. 50 | 51 | .. warning:: UI interface names do not always match with the internal nickname/username. ``!whoami`` is a surefire way of retrieving the correct username. 52 | 53 | On a small scale it's possible to use the ``!whoami`` command to get the correct user account name but for large installations it'd make more sense to use pre-defined patterns. 54 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "err-stackstorm" 21 | copyright = "2019-2022, err-stackstorm contributors" 22 | author = "err-stackstorm contributors" 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = "2.2.0" 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | master_doc = "index" 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ["_templates"] 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | # html_theme = 'alabaster' 51 | html_theme = "sphinx_rtd_theme" 52 | 53 | # Add any paths that contain custom static files (such as style sheets) here, 54 | # relative to this directory. They are copied after the builtin static files, 55 | # so a file named "default.css" will overwrite the builtin "default.css". 56 | html_static_path = ["_static"] 57 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | .. _configuration: 2 | 3 | ************************************************************************ 4 | Configuration 5 | ************************************************************************ 6 | 7 | .. contents:: :local: 8 | 9 | General 10 | ------------------------------------------------------------------------ 11 | 12 | ``err-stackstorm`` configuration is found in Errbot's ``config.py`` file. 13 | 14 | .. note:: If you followed the Errbot setup documentation this file will have been created by downloading a template. If this file has not already been created, please create it following the `Errbot's instructions `_. 15 | 16 | Here's a sample err-stackstorm configuration: 17 | 18 | .. code-block:: python 19 | 20 | STACKSTORM = { 21 | "auth_url": "https://your.stackstorm.com/auth/v1", 22 | "api_url": "https://your.stackstorm.com/api/v1", 23 | "stream_url": "https://your.stackstorm.com/stream/v1", 24 | "route_key": "errbot", 25 | "plugin_prefix": "st2", 26 | "verify_cert": True, 27 | "secrets_store": "cleartext", 28 | "session_ttl": 3600, 29 | "user_token_ttl": 86400, 30 | "api_auth": { 31 | "user": { 32 | "name": "my_username", 33 | "password": "my_password", 34 | }, 35 | "token": "", 36 | "apikey": "" 37 | }, 38 | "rbac_auth": { 39 | "standalone": {}, 40 | }, 41 | "timer_update": 900, # Unit: second. Interval to check the user token is still valid. 42 | } 43 | 44 | ST2 ChatOps Configuration 45 | ------------------------------------------------------------------------ 46 | 47 | StackStorm's `ChatOps pack `_ has to be installed and a notify rule file added to the pack. 48 | 49 | The notify rule must be placed in ``//packs/chatops/rules``. You can find the rule file necessary for `err-stackstorm` `here `_. Just copy-and-paste this file and save it as ``notify_errbot.yaml``. 50 | 51 | Edit the ``//packs/chatops/actions/post_message.yaml`` file and replace `chatops` with ``errbot``: 52 | 53 | .. code-block:: yaml 54 | 55 | route: 56 | default: "errbot" 57 | 58 | .. note:: See Route Key below for information on customising the route. 59 | 60 | Authentication 61 | ------------------------------------------------------------------------ 62 | 63 | `err-stackstorm` must have valid credentials to use StackStorm's API. 64 | StackStorm's authentication is possible with: 65 | 66 | * Username and password 67 | * User token 68 | * API key 69 | 70 | See `StackStorm's Authentication docs `_ for more details on how to generate credentials. 71 | 72 | Username/Password 73 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 74 | 75 | Using a username and password will allow `err-stackstorm` to renew the user token once it expires. 76 | 77 | .. note:: If you specify both username/password and a User Token, the User Token will be used and the *username/password will be ignored*. 78 | 79 | User Token 80 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 81 | 82 | To avoid using the username/password pair in a configuration file, it's possible to supply a User Token as generated by StackStorm when a username/password is authenticated successfully. 83 | 84 | If using an User Token `err-stackstorm` will no longer have access to the StackStorm's API once it expires. 85 | 86 | .. note:: When the token expires, a new one must be generated and updated in config.py which in turn requires Errbot to be restarted. This form of authentication is the least practical for production environments. 87 | 88 | API Key 89 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 90 | 91 | *API Key support has been included since StackStorm v2.0.* 92 | 93 | When an API Key is provided, all other authentication settings are ignored and the API Key is used. 94 | 95 | If using an API Key, `err-stackstorm` will no longer have access to the StackStorm's API once it expires. 96 | 97 | .. note:: It is considered a mistake to supply a token or username/password pair when using the API Key. 98 | 99 | Secrets Store 100 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 101 | 102 | The secrets store is used by `err-stackstorm` to cache StackStorm API credentials. The available backends are: 103 | 104 | * cleartext 105 | 106 | 107 | Cleartext 108 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 109 | 110 | The cleartext store maintains the cache in memory and does not encrypt the contents to disk. It **does not** protect the stored secrets in memory. 111 | 112 | Advanced Options 113 | ------------------------------------------------------------------------ 114 | 115 | Route Key 116 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 117 | 118 | The route key is used by err-stackstorm to inform StackStorm where to send result notifications for action-aliases. StackStorm sends notification events via the stream interface that are marked with the route key. Err-stackstorm filters these events using the route key and will handle any events that match its configured route key. 119 | 120 | By altering the route key, it is possible to have multiple instances of err-stackstorm that are connected to the same StackStorm instance. This would allow for configurations where StackStorm is available on multiple chat backends. 121 | 122 | Example 123 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 124 | A StackStorm instance has 2 err-stackstorm instances. The first err-stackstorm instance uses the `errbot-slack` route key while the second instance uses the `errbot-discord` route key. Both instances have the same plugin prefix and expose the same action-aliases. This would mean the command `!st2 pack list` could be run on discord and the result notification would be routed to the `errbot-discord` err-stackstorm instance. 125 | 126 | 127 | Plugin Prefix 128 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 129 | 130 | By default the plugin prefix is set to `st2`. The plugin prefix serves to prevent action-aliases collisions with errbot's native plugins commands. It is possible to customise the plugin prefix to use a more appropriate naming scheme for the environment err-stackstorm is running. 131 | 132 | Aside from cosmetic value, customising the plugin prefix can allow for multiple err-stackstorm instances to occupy the same chat channel. This would be achieved by setting a unique plugin-prefix per instance. 133 | 134 | .. note:: Always use strings that do not conflict with existing errbot commands. 135 | 136 | Example 137 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 138 | In the case of multiple instances of a StackStorm and err-stackstorm pair, say 1 per data centre or 1 per region, it would be possible to assign a unique plugin prefix per instance. Let's say there were 2 pairs with the plugin-prefix of `dc1` and `dc2`. Both bots could occupy `#automation` channel and users could trigger the pack list action-alias in data centre #1 by calling `!dc1 pack list` or get available action-aliases from data centre #2 by calling `!dc2help`. 139 | 140 | Session TTL 141 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 142 | 143 | The session time to live is an internal timeout for err-stackstorm. It is used to set the maximum lifetime an err-stackstorm session is permitted to exist. Once the session expires the user will need to re-authenticate before being able to execute action-alias commands. An err-stackstorm session ttl should be equal to or less than the user token ttl. 144 | 145 | When session time to live expires, err-stackstorm will report to the user that the session is no longer valid and they should re-authenticate. 146 | 147 | User Token TTL 148 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 149 | 150 | The user token ttl is used to set the maximum life time a StackStorm User Token will be permitted to exist. Once the user token has expired the user will need to re-authenticate before being able to execute action-alias commands. 151 | 152 | When a user token time to live expires, err-stackstorm will report it as an error communicating with the StackStorm API. It would be more user-friendly to ensure the session ttl expires before the user token ttl. 153 | 154 | .. note:: The user token ``ttl`` *must be* equal to or lower than the StackStorm API https://docs.stackstorm.com/authentication.html?highlight=ttl#usage. By default StackStorm's token ttl is set to 24 hours, but the value can be increased through ``st2.conf``. If user_token_ttl is greater than the StackStorm API token ttl value, err-stackstorm will fail to fetch a valid API token and not function correctly. 155 | 156 | Locale 157 | ------------------------------------------------------------------------ 158 | 159 | Errbot uses the system's locale for handling text. If you're getting unicode errors like this:: 160 | 161 | UnicodeEncodeError: 'ascii' codec can't encode character '\xe9' in position 83: ordinal not in range(128) 162 | 163 | Make sure the systems locale is configured for unicode encoding. In the example below, the machine has been set to English (en) New Zealand (NZ) with utf-8 encoding (.UTF8). 164 | 165 | .. code-block:: bash 166 | 167 | # locale 168 | LANG=en_NZ.UTF8 169 | LANGUAGE= 170 | LC_CTYPE="en_NZ.UTF8" 171 | LC_NUMERIC="en_NZ.UTF8" 172 | LC_TIME="en_NZ.UTF8" 173 | LC_COLLATE="en_NZ.UTF8" 174 | LC_MONETARY="en_NZ.UTF8" 175 | LC_MESSAGES="en_NZ.UTF8" 176 | LC_PAPER="en_NZ.UTF8" 177 | LC_NAME="en_NZ.UTF8" 178 | LC_ADDRESS="en_NZ.UTF8" 179 | LC_TELEPHONE="en_NZ.UTF8" 180 | LC_MEASUREMENT="en_NZ.UTF8" 181 | LC_IDENTIFICATION="en_NZ.UTF8" 182 | LC_ALL=en_NZ.UTF8 183 | 184 | 185 | Reference 186 | ------------------------------------------------------------------------ 187 | 188 | 189 | .. csv-table:: 190 | :header: "Option", "Description" 191 | :widths: 25, 40 192 | 193 | "auth_url", "StackStorm's authentication url end point. Used to authenticate credentials against StackStorm." 194 | "api_url", "StackStorm's API url end point. Used to execute action aliases received from the chat back-end." 195 | "stream_url", "StackStorm's Stream url end point. Used to received ChatOps notifications." 196 | "verify_cert", "Default is *True*. Verify the SSL certificate is valid when using https end points. Applies to all end points." 197 | "route_key", "Default is *errbot*. The name of the route to bot will listen for and submit action-alias executions with." 198 | "plugin_prefix", "Default is *st2*. Text used to prefix action-alias commands with to avoid name collisions between StackStorm Action-Aliases and Errbot plugin commands." 199 | "api_auth.user.name", "Errbot's username to authenticate with StackStorm." 200 | "api_auth.user.password", "Errbot's password to authenticate with StackStorm." 201 | "api_auth.token", "Errbot's user token to authenticate with StackStorm. Used instead of a username/password pair." 202 | "api_auth.apikey", "Errbot API key to authenticate with StackStorm. Used instead of a username/password pair or user token." 203 | "timer_update", "Unit: seconds. Default: 60. Interval for err-stackstorm to the user token is valid." 204 | "rbac_auth.standalone", "Standalone authentication." 205 | "rbac_auth.clientside", "Clientside authentication, a chat user will supply StackStorm credentials to err-stackstorm via an authentication page." 206 | "rbac_auth.clientside.url", "Url to the authentication web page." 207 | "session_ttl", "Unit: seconds. Default: 3600. The time to live for a authentication session." 208 | "user_token_ttl", "Unit: seconds. Default: 86400. The time to live for a StackStorm user token." 209 | "secrets_store.cleartext", "Use the in-memory store." 210 | 211 | -------------------------------------------------------------------------------- /docs/dev_references.rst: -------------------------------------------------------------------------------- 1 | ************************************************************************ 2 | Reference material 3 | ************************************************************************ 4 | 5 | Chatops commands must traverse multiple software stacks when being sent from the users chat device 6 | through to StackStorm's backend and back again. The information here are based on observations of the 7 | interactions between the adjacent components. 8 | 9 | This information on this page is very raw and may be out of date. They're included as hints during 10 | exploration of functionality between StackStorm and Errbot. 11 | 12 | 13 | Stackstorm client API 14 | ------------------------------------------------------------------------ 15 | 16 | ActionAlias match 17 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 18 | 19 | When st2client matches an actionalias, an object is returned with the following methods 20 | :: 21 | 22 | class 'st2client.models.action_alias.ActionAlias'> 23 | ral_display_name' 24 | 'ack' 25 | 'action_ref' 26 | 'description' 27 | 'deserialize' 28 | 'enabled' 29 | 'extra' 30 | 'formats' 31 | 'get_alias' 32 | 'get_display_name' 33 | 'get_plural_display_name' 34 | 'get_plural_name' 35 | 'get_url_path_name' 36 | 'id' 37 | 'name' 38 | 'pack' 39 | 'ref' 40 | 'result' 41 | 'serialize' 42 | 'to_dict' 43 | 'uid' 44 | 45 | ack={'enabled': False} 46 | action_ref=livestatus.state_overview 47 | description=Get an overview of Service states via ChatOps 48 | deserialize=> 50 | enabled=True 51 | extra={} 52 | formats=['shinken service overview'] 53 | get_alias=> 55 | get_display_name=> 57 | get_plural_display_name=> 59 | get_plural_name=> 61 | get_url_path_name=> 63 | id=5830ccebdc599a4f6bcef869 64 | name=state_overview 65 | pack=livestatus 66 | ref=livestatus.state_overview 67 | result={'format': 'Standard out says {{ execution.result.result }} ... \\o/'} 68 | serialize=> 70 | to_dict=> 72 | uid=action:livestatus:state_overview 73 | 74 | 75 | API call to alias_execution 76 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 77 | 78 | :: 79 | 80 | curl -X POST 81 | -H 'Connection: keep-alive' 82 | -H 'Accept-Encoding: gzip, deflate' 83 | -H 'Accept: */*' 84 | -H 'User-Agent: python-requests/2.11.1' 85 | -H 'content-type: application/json' 86 | -H 'X-Auth-Token: XXXX' 87 | -H 'Content-Length: 200' 88 | --data-binary ' 89 | { 90 | "notification_channel": "@prime", 91 | "name": "state_overview", 92 | "source_channel": "@ch", 93 | "notification_route": "errbot", 94 | "format": "shinken service overview", 95 | "command": "shinken service overview", 96 | "user": "@ch" 97 | }' 98 | http://127.0.0.1:9101/v1/aliasexecution 99 | 100 | 101 | 102 | Errbot 103 | ------------------------------------------------------------------------ 104 | 105 | Message from slack channel to bot in channel. 106 | :: 107 | 108 | _from [] #ops/ch 109 | _extras [] { 110 | 'slack_event': { 111 | 'text': '.st2 test notify test test', 112 | 'source_team': 'T0V6H6HCJ', 113 | 'ts': '1506953521.000382', 114 | 'user': 'U110FGZSQ', 115 | 'type': 'message', 116 | 'channel': 'C110T9SMT', 117 | 'team': 'T0V6H6HCJ' 118 | }, 119 | 'attachments': None, 120 | 'url': 'https://infradmtest.slack.com/archives/ops/p1506953521000382' 121 | } 122 | _flow [] None 123 | _body [] test notify test test 124 | _parent [] None 125 | ctx [] {} 126 | _delayed [] False 127 | _to [] #ops 128 | 129 | 130 | [ 131 | { 132 | "name": "notify_errbot_test", 133 | "notification_route": "errbot", 134 | "command": "test notify test test", 135 | "format": "test notify {{tag_key}} {{tag_value}}", 136 | "user": "#ops/ch", 137 | "source_channel": "#ops", 138 | "notification_channel": "#ops" 139 | } 140 | ] 141 | 142 | 143 | 144 | Message received from Slack backend 145 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 146 | 147 | :: 148 | 149 | msg.body 150 | msg.ctx 151 | msg.delayed 152 | msg.extras 153 | msg.flow 154 | msg.frm 155 | msg.is_direct 156 | msg.is_group 157 | msg.to 158 | 159 | 160 | Private chat to bot. 161 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 162 | 163 | :: 164 | 165 | msg.body = shinken service overview 166 | msg.ctx = {} 167 | msg.delayed = False 168 | msg.extras = {'attachments': None} 169 | msg.flow = @ch 170 | msg.frm = @ch 171 | msg.is_direct = True 172 | msg.is_group = False 173 | msg.to = @prime 174 | 175 | 176 | Channel chat to bot. 177 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 178 | :: 179 | 180 | msg.body = shinken service overview 181 | msg.ctx = {} 182 | msg.delayed = False 183 | msg.extras = {'attachments': None} 184 | msg.flow = #ops/ch 185 | msg.frm = #ops/ch 186 | msg.is_direct = False 187 | msg.is_group = True 188 | msg.to = #ops 189 | 190 | 191 | From channel 192 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 193 | :: 194 | 195 | msg=['.st2 shinken service overview', 196 | {}, 197 | False, 198 | {'attachments': None}, 199 | , 200 | , 201 | True, 202 | False, 203 | ], 204 | match=<_sre.SRE_Match object; span=(0, 28), match='st2 shinken service overview'> 205 | 206 | SlackPerson 207 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 208 | :: 209 | 210 | channelid=D11LRK2LF, 211 | channelname=D11LRK2LF, 212 | client=D11LRK2LF, 213 | domain=XXXXXXX, 214 | fullname=First Last, 215 | nick=ch, 216 | person=@ch, 217 | userid=U110FGZSQ, 218 | username=ch 219 | 220 | 221 | Stackstorm trigger 222 | ------------------------------------------------------------------------ 223 | 224 | st2.generic.notifytrigger 225 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 226 | :: 227 | 228 | { 229 | "type": "object", 230 | "properties": { 231 | "status": {}, 232 | "start_timestamp": {}, 233 | "route": {}, 234 | "runner_ref": {}, 235 | "execution_id": {}, 236 | "action_ref": {}, 237 | "data": {}, 238 | "message": {}, 239 | "channel": {}, 240 | "end_timestamp": {} 241 | } 242 | } 243 | 244 | 245 | Microsoft Teams 246 | ------------------------------------------------------------------------ 247 | 248 | As of 30 April 2021, the official MS documentation suggests using Visual Studio with the MS Teams Toolkit. 249 | 250 | If that gets you there, perfect. If you don't use or want Visual Studio there's an alternative process. 251 | 252 | Pre-requisites: 253 | - A Microsoft account. 254 | - A MS Teams environment. 255 | 256 | 1. Log in to the Bot Framework website at https://dev.botframework.com/bots/new 257 | 2. Fill in all the required fields. 258 | 259 | 260 | References 261 | ------------------------------------------------------------------------ 262 | 263 | https://techcommunity.microsoft.com/t5/teams-developer/register-bot-without-azure-process/m-p/1490808 264 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | .. _getting_started: 2 | 3 | **************** 4 | Getting Started 5 | **************** 6 | 7 | .. contents:: :local: 8 | 9 | Overview 10 | ========= 11 | 12 | `err-stackstorm` is a community project to bring StackStorm `ChatOps `_ to `Errbot `_. `err-stackstorm` enables StackStorm's `Action Aliases `_ in your chat environment, where you and your team can manage aspects of infrastructure, code and 3rd party services. 13 | 14 | The objectives for err-stackstorm project are: 15 | 1. Provide a Python friendly ChatOps solution. 16 | 2. Maintain the same high quality as the StackStorm project. 17 | 3. Collaborate with the StackStorm community to evolve ChatOps features. 18 | 19 | 20 | Features 21 | ======== 22 | 23 | err-stackstorm communicates directly with the StackStorm API from with an errbot instance. 24 | 25 | - List action-alias help dynamically. When StackStorm action-aliases are updated, they are immediately available in the err-stackstorm output. Filtering by pack name and keyword can be used when looking for help. 26 | - Access-Control Lists based on any combination of chat username, command and room. ACLs are defined in the errbot configuration file. 27 | - Associate StackStorm user credentials with chat usernames. Client-Side authenticate lets err-stackstorm dynamically map chat user accounts with StackStorm authenticated users. Credentials are passed via an out of band authentication web page. 28 | - Trigger action-aliases directly from the bot. 29 | - Support for multiple chat backends, as provided by errbot. 30 | - Customise plugin prefix to allow more than one bot to occupy the same chat channel. 31 | - Customise StackStorm route key to allow more than one bot to be connected to a single StackStorm instance. 32 | - Docker build file available to get up and running quickly and easily. 33 | - Python based using modern 3.x features. 34 | 35 | Compatibility 36 | ======================================================================== 37 | 38 | err-stackstorm v2.2 is compatible with Python 3.7+ and operates with StackStorm v3.0.0 and newer 39 | 40 | 41 | Platforms 42 | ========= 43 | 44 | ``err-stackstorm`` is developed and testing on the x86_64 platform for Linux. No other platforms or operating systems have been tested. 45 | 46 | .. important:: ``err-stackstorm`` has been reported by users to have issue running on Mac OSX. 47 | 48 | Supported Chat Backends 49 | ======================================================================== 50 | 51 | Errbot provides official support for most major chat back-ends and many more chat back-ends are available through unofficial community plugins. 52 | 53 | 54 | .. csv-table:: Available Chat Backends 55 | :header: "Service (backend)", "Backend mode name", "Support Type" 56 | :widths: 10, 10, 10 57 | 58 | "Slack", ``SlackV3``, "`slackv3 plugin `_" 59 | "Mattermost", ``mattermost``, "`mattermost plugin `_" 60 | "Discord", ``discord``, "`discord plugin `_" 61 | "Rocket Chat", ``aoikrocketchaterrbot``, "`rocket chat plugin `_" 62 | "Gitter", ``gitter``, "`gitter plugin `_" 63 | "XMPP", ``xmpp``, "Included with errbot" 64 | "IRC", ``irc``, "Included with errbot" 65 | 66 | Despite errbot having support for some backend chat services, they are not directly supported by err-stackstorm. 67 | 68 | .. csv-table:: Unsupported Chat Backends 69 | :header: "Service (backend)", "Backend mode name", "Support Type" 70 | :widths: 10, 10, 10 71 | 72 | "MSTeam", ``BotFramework``, "`msteams plugin `_" 73 | "Google Chat", ``Google_Hangouts_Chat``, "`google chat plugin `_" 74 | 75 | .. important:: Microsoft Teams and Google Chat are available in errbot but are not supported by err-stackstorm because the maintainer (nzlosh) has no access to these services. If you wish to help maintain support for these backends in err-stackstorm contact nzlosh. 76 | 77 | Backend support provides a minimum set of operations such as `connect` and `authentication` methods along with ways to `identify` and `send messages` to users/rooms. 78 | 79 | Advanced formatting may not be available on all backends since additional code is required in `err-stackstorm` to translate StackStorm's Action Aliases `extra` parameter to advanced formatting in the backend. 80 | 81 | Backends that currently support nice (extra) formatting: 82 | 83 | * Slack 84 | * XMPP 85 | -------------------------------------------------------------------------------- /docs/images/authentication_screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nzlosh/err-stackstorm/32fa23a5821bcbccd05c2e65e89b1aaec9402c9e/docs/images/authentication_screen.jpg -------------------------------------------------------------------------------- /docs/images/remote_shell.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nzlosh/err-stackstorm/32fa23a5821bcbccd05c2e65e89b1aaec9402c9e/docs/images/remote_shell.jpg -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. err-stackstorm documentation master file, created by 2 | sphinx-quickstart on Fri Jun 14 09:32:01 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. title:: err-stackstorm's documentation 7 | 8 | Welcome to err-stackstorm's documentation! 9 | ========================================== 10 | 11 | err-stackstorm is a community project to bring StackStorm ChatOps to Errbot. 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | :caption: Contents: 16 | 17 | getting_started.rst 18 | quick_start.rst 19 | installation.rst 20 | configuration.rst 21 | authn.rst 22 | authz.rst 23 | action_aliases.rst 24 | troubleshooting.rst 25 | project.rst 26 | show_case.rst 27 | linux_package_building.rst 28 | dev_references.rst 29 | 30 | Indices and tables 31 | ================== 32 | 33 | * :ref:`genindex` 34 | * :ref:`modindex` 35 | * :ref:`search` 36 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | ************************************************************************ 4 | Installation 5 | ************************************************************************ 6 | 7 | .. contents:: :local: 8 | 9 | ``err-stackstorm`` can be installed using system packages or by following installation instructions. 10 | It is recommended to use the system packages. Manual installation instructions are provided below. 11 | 12 | System Package 13 | ------------------------------------------------------------------------ 14 | 15 | System packages are provided to install ``err-stackstorm`` easily in an isolated Python Virtual Environment. 16 | 17 | Ubuntu 18 | Installation is available for bionic, focal and jammy. 19 | 20 | 1. Add apt repository 21 | 22 | :: 23 | 24 | sudo echo 'deb https://nzlosh.github.io/repo/ubuntu bionic main' > /etc/apt/source.list.d/err-stackstorm.list 25 | 26 | 2. Import public key 27 | 28 | :: 29 | 30 | curl https://nzlosh.github.io/repo/repo_nzlosh_public.gpg | gpg --import - 31 | 32 | 3. Update apt indexes 33 | 34 | :: 35 | 36 | apt update 37 | 38 | 4. Install err-stackstorm 39 | 40 | :: 41 | 42 | apt install err-stackstorm 43 | 44 | 45 | Debian 46 | Installation is available for buster and bullseye. 47 | 48 | 1. Add apt repository 49 | 50 | :: 51 | 52 | sudo echo 'deb https://nzlosh.github.io/repo/debian buster main' > /etc/apt/source.list.d/err-stackstorm.list 53 | 54 | 2. Import public key 55 | 56 | :: 57 | 58 | curl https://nzlosh.github.io/repo/repo_nzlosh_public.gpg | gpg --import - 59 | 60 | 3. Update apt indexes 61 | 62 | :: 63 | 64 | apt update 65 | 66 | 4. Install err-stackstorm 67 | 68 | :: 69 | 70 | apt install err-stackstorm 71 | 72 | CentOS 73 | Installation is available for CentOS 8. 74 | 75 | In the new file, enter the command (replacing the IP address with the IP address of your server): 76 | 77 | 1. add the yum repository 78 | 79 | :: 80 | 81 | [remote] 82 | name=err-stackstorm 83 | baseurl=https://nzlosh.github.io/repo/rhel/8/ 84 | enabled=1 85 | gpgcheck=1 86 | 87 | Rocky 88 | Installation is available in Rocky 9. 89 | 90 | 1. Add a new repository using the ``dnf config-manager --add-repo`` command 91 | 92 | :: 93 | 94 | sudo dnf config-manager --add-repo https://nzlosh.github.io/repo/rhel/8/err-stackstorm.repo 95 | 96 | 97 | Python Virtual Environment 98 | ------------------------------------------------------------------------ 99 | 100 | 101 | 102 | Installation of the err-stackstorm plugin can be performed from within a running Errbot instance. Ensure Errbot is up and running before attempting to install the plugin. 103 | 104 | .. note:: See the Errbot installation documentation `here `_ for instructions on how to setup Errbot with the chat backend of your choice. 105 | 106 | .. important:: These instructions assume a running instance of StackStorm is already in place. See the `official StackStorm documentation `_ for details. 107 | 108 | Setting up Errbot's Webserver 109 | ------------------------------------------------------------------------ 110 | 111 | .. note:: Configuring the Web server is optional and not required for err-stackstorm to function correctly. 112 | 113 | `err-stackstorm` uses a webhook that listens for messages to post to the backend. This is a convenience feature to simplify pushing message from StackStorm workflows. `webserver plugin `_. 114 | 115 | Enabling Errbot's webserver 116 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 117 | 118 | Enable the webserver by sending the following command to Errbot:: 119 | 120 | !plugin config Webserver {'HOST': '0.0.0.0', 'PORT': 3141, 'SSL': {'enabled': False, 'host': '0.0.0.0', 'port': 3142, 'certificate': '', 'key': ''}} 121 | 122 | The variables must be adjusted to match the operating environment in which Errbot is running. See `Errbot documentation `_ for further configuration information. 123 | 124 | .. warning:: In production environments it is recommended to place a reverse-proxy like `nginx `_ or `Apache `_ in front of Errbot. 125 | 126 | .. note:: Docker users have reported the webserver does not start with container restarts. It is recommended to install the webserver configuration plugin to enable the webserver in containered environments. ``!repos install https://github.com/tkit/errbot-plugin-webserverconfiguration`` 127 | 128 | 129 | Update Errbot's config.py 130 | ------------------------------------------------------------------------ 131 | 132 | Paste the sample configuration below in Errbot's ``config.py`` file adjusting the URLs to match your StackStorm instance and set up one of the authentication methods. 133 | 134 | .. code-block:: python 135 | 136 | STACKSTORM = { 137 | 'auth_url': 'https://your.stackstorm.com/auth/v1', 138 | 'api_url': 'https://your.stackstorm.com/api/v1', 139 | 'stream_url': 'https://your.stackstorm.com/stream/v1', 140 | 'route_key': 'errbot', 141 | 'plugin_prefix': 'st2', 142 | 'verify_cert': True, 143 | 'secrets_store': 'cleartext', 144 | 'api_auth': { 145 | 'user': { 146 | 'name': 'my_username', 147 | 'password': "my_password", 148 | }, 149 | 'token': "", 150 | 'apikey': '' 151 | }, 152 | 'rbac_auth': { 153 | 'standalone': {}, 154 | }, 155 | 'timer_update': 900, # Unit: second. Interval to check the user token is still valid. 156 | } 157 | 158 | 159 | See :ref:`configuration` for in-depth explanation. 160 | 161 | Installing err-stackstorm 162 | ------------------------------------------------------------------------ 163 | 164 | Confirm Errbot is configured to install plugin dependencies. The below line should be present in Errbot's `config.py`:: 165 | 166 | AUTOINSTALL_DEPS = True 167 | 168 | This line ensures that Errbot will attempt to automatically install the requirements of any plugin you may ask it to install. 169 | 170 | Now install err-stackstorm:: 171 | 172 | !repos install https://github.com/nzlosh/err-stackstorm.git 173 | 174 | The plugin will fail to install if any errors are encountered. This is often caused by configuration errors in Errbot's config.py. 175 | 176 | You can confirm that it installed by typing:: 177 | 178 | !repos list 179 | 180 | The list should contain the ``nzlosh/err-stackstorm`` repo. 181 | -------------------------------------------------------------------------------- /docs/linux_package_building.rst: -------------------------------------------------------------------------------- 1 | Building Linux Packages 2 | ========================================================================================================================================================= 3 | Two scripts are used to help ``err-stackstorm`` packages available on multiple operating systems. Each script represent a phase in the package build process 4 | 5 | ``contrib/packaging/build_archive`` is a BASH script that carries out the steps to create a Python virtual environment and install errbot, various backends and err-stackstorm using a supported operating systems native Python installation. 6 | 7 | ``contrib/packaging/build`` is a Python script that depends on jinja2. It should be setup using its own virtual environment and is executed after the err-stackstorm archive has been successfully created by ``build_archive``. ``build`` reads the ``build.cfg`` configuration file, which is JSON format, to fill out jinja templates that are used by the operating systems package build system. This allows for updating and build system packages in a similar manner for all supported operating systems. 8 | 9 | Phase 1: Packaging Python 10 | --------------------------------------------------------------------------------------------------------------------------------------------------------- 11 | Python applications are best isolated from the system. Python virtual environments (venv) are created for this purpose, the virtual environment is created inside a single directory, various base standard libraries are copied into the venv and symlinks setup to point to the systems Python interpreter. The venv will be created with whichever version of python was executed to create the venv. 12 | :: 13 | 14 | python3.8 -m venv 15 | source /bin/activate 16 | 17 | 18 | Phase 2: Build System packages 19 | --------------------------------------------------------------------------------------------------------------------------------------------------------- 20 | After the Python virtual environment has been setup, dependencies installed and pre-compiled files purged, the Python virtual environment is archived using ``tar``. The resulting archive will be used to deploy the virtual environment inside the native OS packaging. 21 | 22 | Building DEB based packages 23 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 24 | TO DO 25 | 26 | Building RPM based packages 27 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 28 | TO DO 29 | -------------------------------------------------------------------------------- /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% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/project.rst: -------------------------------------------------------------------------------- 1 | .. _project: 2 | 3 | ******** 4 | Project 5 | ******** 6 | 7 | Community 8 | ========= 9 | 10 | You can find users of `err-stackstorm` on: 11 | 12 | * `Gitter `_ 13 | * `Slack `_ on #err-stackstorm channel. 14 | 15 | There's also the comprehensive :ref:`troubleshooting` section to help guide you through possible break/fix scenarios. 16 | 17 | Contributing 18 | ============ 19 | 20 | If you think you've found a bug or need a new feature `open an issue `_ on the `github repository `_. 21 | 22 | If you want to contribute to the err-stackstorm project, there are plenty of improvements to be made, contact nzlosh via chat or email to discuss how you can get involved. 23 | 24 | -------------------------------------------------------------------------------- /docs/quick_start.rst: -------------------------------------------------------------------------------- 1 | .. _quick_start: 2 | 3 | ************************************************************************ 4 | Quick Start 5 | ************************************************************************ 6 | 7 | If you are familiar with Errbot and StackStorm, this guide will get you up and running in no time. For in-depth information, refer to :ref:`installation` and :ref:`configuration` 8 | 9 | 1. Enable Errbot's webserver (the following command must be a private message to the bot by a bot admin) :: 10 | 11 | !plugin config Webserver { 12 | "HOST": "0.0.0.0", 13 | "PORT": 3141, 14 | "SSL": { 15 | "certificate": "", 16 | "enabled": False, 17 | "host": "0.0.0.0", 18 | "key": "", 19 | "port": 3142 20 | } 21 | } 22 | 23 | !plugin activate Webserver 24 | 25 | 2. Paste the sample configuration below in Errbot's ``config.py`` file adjusting the URLs to match your StackStorm instance and set up one of the authentication methods.:: 26 | 27 | STACKSTORM = { 28 | 'auth_url': 'https://your.stackstorm.com/auth/v1', 29 | 'api_url': 'https://your.stackstorm.com/api/v1', 30 | 'stream_url': 'https://your.stackstorm.com/stream/v1', 31 | 'route_key': 'errbot', 32 | 'plugin_prefix': 'st2', 33 | 'verify_cert': True, 34 | 'secrets_store': 'cleartext', 35 | 'api_auth': { 36 | 'user': { 37 | 'name': 'my_username', 38 | 'password': "my_password", 39 | }, 40 | 'token': "", 41 | 'apikey': '' 42 | }, 43 | 'rbac_auth': { 44 | 'standalone': {}, 45 | }, 46 | 'timer_update': 900, # Unit: second. Interval to check the user token is still valid. 47 | } 48 | 49 | 50 | 3. Install err-stackstorm:: 51 | 52 | !repos install https://github.com/nzlosh/err-stackstorm.git 53 | 54 | 6. Set up an `action alias `_ on StackStorm. See :ref:`action_alias` for more details. 55 | 56 | 7. Sending ``!st2help`` to your bot will list the available StackStorm's aliases. 57 | 58 | 8. Aliases can be run like this: ``!st2 run date on 192.168.5.1`` 59 | 60 | .. important:: When restarting StackStorm, a warning may be produced to inform you `st2chatops` is not running. This warning can be ignored because `err-stackstorm` will be managing StackStorm ChatOps. 61 | -------------------------------------------------------------------------------- /docs/show_case.rst: -------------------------------------------------------------------------------- 1 | .. _show_case: 2 | 3 | ************************************************************************ 4 | Show case 5 | ************************************************************************ 6 | 7 | Features 8 | ======================================================================== 9 | 10 | * A single StackStorm instance accessible from multiple chat providers 11 | 12 | * Multiple StackStorm instances accessible from a single chat provider 13 | 14 | * Slack Blocks to create rich content 15 | 16 | * StackStorm inquiries query/response integration 17 | 18 | * Sending/Receiving files in Slack 19 | 20 | * Chat provider integration examples 21 | 22 | 23 | StackStorm ChatOps Examples 24 | 25 | The examples are arrange from simple use cases, such as setting up the first action-alias through 26 | to inquiry response, multiple bot routing across multiple chat providers. 27 | 28 | Send a notification from an action 29 | Execute a chatops command 30 | Executing a chatops command with arguments 31 | Executing a chatops command and returning a result 32 | Formatting a chatops command result using jinja templates 33 | Advanced formatting using chat provider features (Slack blocks) 34 | 35 | 36 | User provided content 37 | ======================================================================== 38 | 39 | Some generous people have kindly taken the time to create content around using the err-stackstorm project. Links have been provided to help others find find the work: 40 | 41 | * ChatOps chronicles - An exciting journey to a wonderland of conversation-driven DevOps by Denys Havrysh https://medium.com/chatops-chronicles 42 | * Introduction to ChatOps. Errbot + Slack integration by Michał Wcisło. https://devopsspiral.com/video/chatops-intro/ 43 | * ChatOps level up with StackStorm by Michał Wcisło https://devopsspiral.com/video/chatops-stackstorm/ 44 | 45 | Thank you to everyone who has contributed to the err-stackstorm project! 46 | -------------------------------------------------------------------------------- /docs/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | .. _troubleshooting: 2 | 3 | **************** 4 | Troubleshooting 5 | **************** 6 | 7 | .. contents:: :local: 8 | 9 | Basic Checks 10 | ============= 11 | 12 | Is the Errbot process running? 13 | ------------------------------- 14 | 15 | Check if an instance of Errbot is running on the host:: 16 | 17 | # ps faux | grep errbo[t] 18 | root 158707 0.1 0.0 2922228 59640 pts/21 Sl+ Aug14 2:29 | \_ /opt/errbot/bin/python3 /opt/errbot/bin/errbot -c /data/errbot/etc/config.py 19 | 20 | Is the Errbot webhook listening? 21 | -------------------------------- 22 | 23 | Check Errbot's internal web server is listening on the correct interface. Use the PID from the previous command to filter the output to find which port the errbot process is listening on. You can use ``ss``:: 24 | 25 | # ss -tlpn | grep 158707 26 | LISTEN 0 128 *:3141 *:* users:(("errbot",158707,21)) 27 | 28 | 29 | or Netstat:: 30 | 31 | # netstat -tlpn | grep 158707 32 | tcp 0 0 0.0.0.0:3141 0.0.0.0:* LISTEN 158707/python3 33 | 34 | 35 | If the result is empty, errbot is not listening on any ports. Refer to the Webserver section in :ref:`installation`. 36 | 37 | 38 | Is the Errbot machine able to communicate with the StackStorm end points? 39 | -------------------------------------------------------------------------- 40 | 41 | From the errbot machine perform a curl to the StackStorm endpoint:: 42 | 43 | curl http:///api/v1/rules 44 | 45 | Are the Errbot authentication credentials for StackStorm correct? 46 | ------------------------------------------------------------------ 47 | 48 | Here's you can test that the authentication is working. 49 | 50 | Username/password 51 | """"""""""""""""" 52 | 53 | A successful username/password authentication is shown below:: 54 | 55 | $ st2 auth errbot 56 | Password: 57 | +----------+----------------------------------+ 58 | | Property | Value | 59 | +----------+----------------------------------+ 60 | | user | errbot | 61 | | token | 10342978da134ae5bbb7dc94d2ba9c08 | 62 | | expiry | 2017-09-29T14:31:20.799212Z | 63 | +----------+----------------------------------+ 64 | 65 | 66 | If the username and password are valid and correctly entered in errbot's configuration file, errbot will be authorised to interact with StackStorm's API/Stream end points. 67 | 68 | User Token 69 | """""""""" 70 | 71 | You can test the user token from the configuration using the ``st2`` command and passing the token with ``-t`` argument. 72 | 73 | .. important:: Make sure no environment variables are set that could provide a valid token or api key already. 74 | 75 | .. code-block:: bash 76 | 77 | $ st2 action-alias list -t 10342978da134ae5bbb7dc94d2ba9c08 78 | +-----------------------------------+------------+---------------------------------------+---------+ 79 | | ref | pack | description | enabled | 80 | +-----------------------------------+------------+---------------------------------------+---------+ 81 | | packs.pack_get | packs | Get information about installed | True | 82 | | | | StackStorm pack. | | 83 | | packs.pack_install | packs | Install/upgrade StackStorm packs. | True | 84 | | packs.pack_search | packs | Search for packs in StackStorm | True | 85 | | | | Exchange and other directories. | | 86 | | packs.pack_show | packs | Show information about the pack from | True | 87 | +-----------------------------------+------------+---------------------------------------+---------+ 88 | 89 | 90 | If a list of action aliases are shown, the token is valid. 91 | 92 | API Key 93 | """"""" 94 | 95 | Confirm the api key has been created and still registered with StackStorm by using it with the st2 command:: 96 | 97 | $ st2 apikey list --api-key ZzVk3DEBZ4FiZmMEmDBkM2x5ZmM5jWZkZWZjZjZmMZEwYzQwZD2iYzUyM2RhYTkTNMYmNDYNODIOOTYwMzE20A 98 | +--------------------------+--------+-------------------------------------------+ 99 | | id | user | metadata | 100 | +--------------------------+--------+-------------------------------------------+ 101 | | 586e6deadbeef66deadbeef6 | errbot | {u'used_by': u'errbot api access'} | 102 | +--------------------------+--------+-------------------------------------------+ 103 | 104 | Is Errbot connected correctly to the chat back-end? 105 | ---------------------------------------------------- 106 | 107 | How to test if the bot is connected to the chat back-end is dependant on the back-end. The simplest way is to send a message to the bot user account requesting the built-in help. 108 | 109 | For example, using a slack client the following command would be used ``/msg @bot_name !help``. The bot should respond with its help text:: 110 | 111 | bot [11:01 AM] 112 | _All commands_ 113 | 114 | *Backup* 115 | _Backup related commands._ 116 | • *.backup* - Backup everything. 117 | *ChatRoom* 118 | _This is a basic implementation of a chatroom_ 119 | • *.room join* - Join (creating it first if needed) a chatroom. 120 | • *.room occupants* - List the occupants in a given chatroom. 121 | • *.room invite* - Invite one or more people into a chatroom. 122 | • *.room topic* - Get or set the topic for a room. 123 | 124 | 125 | Is the StackStorm ChatOps pack installed and configured correctly? 126 | -------------------------------------------------------------------- 127 | 128 | err-stackstorm requires the ChatOps pack to be installed. To confirm it is installed, use the ``st2`` cli. 129 | 130 | .. code-block:: bash 131 | 132 | $ st2 pack list 133 | +-------------------+-------------------+--------------------------------+---------+----------------------+ 134 | | ref | name | description | version | author | 135 | +-------------------+-------------------+--------------------------------+---------+----------------------+ 136 | | chatops | chatops | ChatOps integration pack | 0.2.0 | Kirill Enykeev | 137 | +-------------------+-------------------+--------------------------------+---------+----------------------+ 138 | 139 | 140 | Confirm the ``notify_errbot.yaml`` is inside the ``chatops/rules`` directory:: 141 | 142 | $ cat /opt/stackstorm/packs/chatops/rules/notify_errbot.yaml 143 | 144 | You should see a YAML like the one below:: 145 | 146 | --- 147 | name: "notify-errbot" 148 | pack: "chatops" 149 | enabled: true 150 | description: "Notification rule to send results of action executions to stream for chatops" 151 | trigger: 152 | type: "core.st2.generic.notifytrigger" 153 | criteria: 154 | trigger.route: 155 | pattern: "errbot" 156 | type: "equals" 157 | action: 158 | ref: chatops.post_result 159 | parameters: 160 | channel: "{{ trigger.data.source_channel }}" 161 | user: "{{ trigger.data.user }}" 162 | execution_id: "{{ trigger.execution_id }}" 163 | 164 | The rule should be available using command ``st2 rule get chatops.notify-errbot`` 165 | 166 | .. code-block:: bash 167 | 168 | +-------------+--------------------------------------------------------------+ 169 | | Property | Value | 170 | +-------------+--------------------------------------------------------------+ 171 | | id | 5a6b1abc5b3a0f0f5bcd54e7 | 172 | | uid | rule:chatops:notify-errbot | 173 | | ref | chatops.notify-errbot | 174 | | pack | chatops | 175 | | name | notify-errbot | 176 | | description | Notification rule to send results of action executions to | 177 | | | stream for chatops | 178 | | enabled | True | 179 | | action | { | 180 | | | "ref": "chatops.post_result", | 181 | | | "parameters": { | 182 | | | "user": "{{trigger.data.user}}", | 183 | | | "execution_id": "{{trigger.execution_id}}", | 184 | | | "channel": "{{trigger.data.source_channel}}" | 185 | | | } | 186 | | | } | 187 | | criteria | { | 188 | | | "trigger.route": { | 189 | | | "pattern": "errbot", | 190 | | | "type": "equals" | 191 | | | } | 192 | | | } | 193 | | tags | | 194 | | trigger | { | 195 | | | "type": "core.st2.generic.notifytrigger", | 196 | | | "ref": "core.st2.generic.notifytrigger", | 197 | | | "parameters": {} | 198 | | | } | 199 | | type | { | 200 | | | "ref": "standard", | 201 | | | "parameters": {} | 202 | | | } | 203 | +-------------+--------------------------------------------------------------+ 204 | 205 | 206 | 207 | Are events being sent via the StackStorm Stream? 208 | ------------------------------------------------ 209 | 210 | From the errbot host connect to the StackStorm stream endpoint and watch for events emitted as actions are executed by StackStorm:: 211 | 212 | curl -s -v -H 'Accept: text/event-stream' -H 'X-Auth-Token: 10342978da134ae5bbb7dc94d2ba9c08' http:///stream/v1/stream 213 | 214 | The correct URL will depend on your StackStorm installation, the URL must corresponds to https://api.stackstorm.com/stream/v1/stream/ 215 | 216 | 217 | Are the events seen in the errbot logs using errbot as their route? 218 | -------------------------------------------------------------------- 219 | 220 | To see the events in the log, the debug level ``BOT_LOG_LEVEL = logging.DEBUG`` will need to be added to errbot's configuration file ``config.py``. 221 | 222 | If events are configured correctly, logs will be shown like this (``st2.announcement__errbot``):: 223 | 224 | 17:04:12 DEBUG root Dispatching st2.announcement__errbot event, 990 bytes... 225 | 17:04:12 DEBUG lib.st2pluginapi *** Errbot announcement event detected! *** 226 | st2.announcement__errbot event, 990 bytes 227 | 228 | If the announcement event is showing as:: 229 | 230 | 2018-01-26 15:51:55,246 DEBUG sseclient Dispatching st2.announcement__chatops event, 508 bytes... 231 | 232 | This indicates that the route wasn't set to errbot, refer to :ref:`configuration`. 233 | 234 | 235 | 236 | F.A.Q. 237 | ====== 238 | 239 | 240 | running errbot returns command not found 241 | ----------------------------------------- 242 | 243 | If Errbot was installed in a python virtual environment, make sure the virtual environment is activated correctly. 244 | 245 | st2chatops is not running 246 | -------------------------- 247 | 248 | The `st2ctl` command is designed with the assumption that ``st2chatops`` is installed for St2 ChatOps. Since ``err-stackstorm`` **replaces** ``st2chatops``, this 249 | error message can be safely ignored. More over, ``err-stackstorm`` does not require to be restarted when new action aliases are added since they're read at runtime 250 | from the API. 251 | 252 | 253 | webserver configuration doesn't persist between bot restarts 254 | ------------------------------------------------------------- 255 | 256 | In some environments which have file system restrictions like containers, errbot can't save plugin configuration to the data store. This in turn causes the web server configuration to be lost when the bot is restarted. To work around this limitation, a plugin that loads the web servers configuration on bot startup can be used. Install it with the following command: 257 | 258 | !repos install https://github.com/tkit/errbot-plugin-webserverconfiguration 259 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=61.0" 4 | ] 5 | build-backend = "setuptools.build_meta" 6 | 7 | [project] 8 | name = "err-stackstorm" 9 | version = "2.2.1" 10 | authors = [{ name="Err-stackstorm maintainers", email="nzlosh@yahoo.com" }] 11 | keywords = [ 12 | "errbot", 13 | "stackstorm", 14 | ] 15 | description = "StackStorm ChatOps for Errbot" 16 | readme = "README.md" 17 | requires-python = ">=3.7" 18 | 19 | license = {text = "Apache-2.0"} 20 | 21 | classifiers = [ 22 | "Programming Language :: Python :: 3", 23 | "License :: OSI Approved :: Apache Software License", 24 | "Operating System :: OS Independent", 25 | ] 26 | 27 | dependencies = [ 28 | "requests>=2.20.0", 29 | "sseclient", 30 | "jsonschema", 31 | ] 32 | 33 | [project.urls] 34 | "Homepage" = "https://github.com/nzlosh/err-stackstorm" 35 | "Bug Tracker" = "https://github.com/nzlosh/err-stackstorm/issues" 36 | "Documentation" = "https://err-stackstorm.readthedocs.io/" 37 | 38 | [tool.setuptools] 39 | # available as beta since setuptools version 61.0.0 40 | include-package-data = true 41 | 42 | [tool.pytest.ini_options] 43 | pythonpath = [ 44 | "src/err-stackstorm" 45 | ] 46 | 47 | [tool.black] 48 | line-length = 100 49 | -------------------------------------------------------------------------------- /requirements-build.txt: -------------------------------------------------------------------------------- 1 | build 2 | twine 3 | -------------------------------------------------------------------------------- /requirements-docs.txt: -------------------------------------------------------------------------------- 1 | sphinx-rtd-theme==0.4.2 2 | Sphinx==1.8.3 3 | 4 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | mock 3 | pycodestyle 4 | black 5 | bandit 6 | -------------------------------------------------------------------------------- /src/err-stackstorm/errst2lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nzlosh/err-stackstorm/32fa23a5821bcbccd05c2e65e89b1aaec9402c9e/src/err-stackstorm/errst2lib/__init__.py -------------------------------------------------------------------------------- /src/err-stackstorm/errst2lib/authentication_controller.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import logging 3 | 4 | from errbot.backends.base import Identifier 5 | 6 | from errst2lib.errors import SessionInvalidError 7 | from errst2lib.session import generate_password 8 | from errst2lib.session_manager import SessionManager 9 | 10 | LOG = logging.getLogger("errbot.plugin.st2.auth_ctrl") 11 | 12 | 13 | class BotPluginIdentity(object): 14 | """ 15 | For internal use only by err-stackstorm. The object is used by methods that will create a 16 | session and authenticate err-stackstorm credentials with StackStorm. 17 | """ 18 | 19 | def __init__(self, name="errbot%service", secret=generate_password(16)): 20 | self.name = name 21 | self.secret = secret 22 | 23 | def __repr__(self): 24 | return str(self.secret) 25 | 26 | 27 | class AuthenticationController(object): 28 | def __init__(self, bot): 29 | self.bot = bot 30 | self.sessions = SessionManager(bot.cfg) 31 | 32 | def to_userid(self, user): 33 | """ 34 | Convert BotIdentity, Identifier to plain text string suitable for use as the key with 35 | Sessions and cached tokens. 36 | param: user: may be one of BotIdentity, errbot.backend.base.Identifier or string. 37 | """ 38 | if isinstance(user, BotPluginIdentity): 39 | user_id = user.name 40 | elif isinstance(user, Identifier): 41 | user_id = self.bot.chatbackend.normalise_user_id(user) 42 | else: 43 | user_id = user 44 | LOG.debug("Authentication User ID is '{}'".format(user_id)) 45 | return user_id 46 | 47 | def pre_execution_authentication(self, chat_user): 48 | """ 49 | Look up the chat_user to confirm they are authenticated. 50 | param: chat_user: the chat back end user. 51 | return: A valid St2 Token or False in the case of an error 52 | """ 53 | user_id = self.to_userid(chat_user) 54 | return self.bot.cfg.auth_handler.pre_execution_authentication(self, user_id) 55 | 56 | def consume_session(self, session_id): 57 | """ 58 | Fetch the session and unseal it to mark it as consumed. 59 | """ 60 | session = self.sessions.get_by_uuid(session_id) 61 | if session is False: 62 | LOG.debug("Invalid session id '{}'.".format(session_id)) 63 | raise SessionInvalidError 64 | session.unseal() 65 | return True 66 | 67 | def list_sessions(self): 68 | """ 69 | Returns a list of sessions. 70 | """ 71 | return self.sessions.list_sessions() 72 | 73 | def session_url(self, session_id, url_path="/"): 74 | """ 75 | Return a URL formatted with the UUID query string attached. 76 | """ 77 | return "{}{}?uuid={}".format(self.bot.cfg.auth_handler.url, url_path, session_id) 78 | 79 | def delete_session(self, session_id): 80 | """ 81 | Delete a session from the store. 82 | """ 83 | session = self.sessions.get_by_uuid(session_id) 84 | if session is False: 85 | LOG.debug("Session '{}' doesn't exist to be deleted".format(session_id)) 86 | raise SessionInvalidError 87 | else: 88 | self.sessions.delete(session.user_id) 89 | 90 | def get_session_userid(self, session_id): 91 | session = self.sessions.get_by_uuid(session_id) 92 | session.is_expired() 93 | return session.user_id 94 | 95 | def get_token_by_userid(self, user): 96 | """ 97 | Get the associated StackStorm token/key given chat backend username. 98 | Return StackStorm token/key associated with the user or False if session isn't valid or 99 | secret is missing. 100 | """ 101 | secret = False 102 | session = self.sessions.get_by_userid(self.to_userid(user)) 103 | if session: 104 | secret = self.get_token_by_session(session.id()) 105 | return secret 106 | 107 | def get_token_by_session(self, session_id): 108 | """ 109 | Get the associated StackStorm token/key given session id. 110 | """ 111 | LOG.debug("Get token for session id {}".format(session_id)) 112 | return self.sessions.get_secret(session_id) 113 | 114 | def set_token_by_session(self, session_id, token): 115 | """ 116 | Stores a StackStorm user token or api key in the secrets store using the session id. 117 | """ 118 | return self.sessions.put_secret(session_id, token) 119 | 120 | def set_token_by_userid(self, user_id, token): 121 | """ 122 | Store StackStorm user token or api key in the secrets store. 123 | """ 124 | session = self.sessions.get_by_userid(self.to_userid(user_id)) 125 | if session: 126 | ret = self.set_token_by_session(session.id(), token) 127 | else: 128 | LOG.debug("Failed to lookup session for user id '{}'".format(user_id)) 129 | return ret 130 | 131 | def create_session(self, user, user_secret): 132 | """ 133 | Handle an initial request to establish a session. If a session already exists, return it. 134 | """ 135 | user_id = self.to_userid(user) 136 | return self.sessions.create(user_id, user_secret, self.bot.cfg.session_ttl) 137 | 138 | def get_session(self, user): 139 | """ 140 | Returns the session associated with the user. 141 | """ 142 | user_id = self.to_userid(user) 143 | session = self.sessions.get_by_userid(user_id) 144 | if session is False: 145 | raise SessionInvalidError 146 | return session 147 | 148 | def match_secret(self, session_id, user_secret): 149 | """ 150 | Fetch session and compare user_secret. 151 | """ 152 | session = self.sessions.get_by_uuid(session_id) 153 | if session is False: 154 | LOG.debug("Session '{}' doesn't exist.".format(session_id)) 155 | raise SessionInvalidError 156 | 157 | if session.is_sealed(): 158 | LOG.warning("Attempting to check secret while session is sealed.") 159 | return False 160 | 161 | return session.match_secret(user_secret) 162 | 163 | def associate_credentials(self, user, creds, bot_creds): 164 | """ 165 | Verify credentials against stackstorm and if successful, store them using the user id. 166 | param: user: the normalised chat_user account. 167 | param: creds: the stackstorm user credentials to validate against StackStorm API. 168 | param: bot_creds: the bot credentials to use when authenticating user credentials. 169 | Return true if credentials were valid or False if they were not. 170 | """ 171 | # get the configured authentication handler. 172 | token = self.bot.cfg.auth_handler.authenticate(user, creds, bot_creds) 173 | 174 | # WARNING: Sensitive security information will be logged, uncomment only when necessary. 175 | # LOG.debug("Token for {} was {}".format(user, token)) 176 | 177 | # pass credentials to authentication handler verify credentials 178 | if token: 179 | self.set_token_by_userid(user, token) 180 | else: 181 | LOG.warning("Failed to validate StackStorm credentials for {}.".format(user)) 182 | # Explicitly test not false to avoid returning tokens value. 183 | return token is not False 184 | -------------------------------------------------------------------------------- /src/err-stackstorm/errst2lib/config.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import logging 3 | 4 | from errst2lib.authentication_handler import AuthHandlerFactory 5 | from errst2lib.credentials_adapters import CredentialsFactory 6 | 7 | LOG = logging.getLogger(__name__) 8 | 9 | 10 | class BorgSingleton: 11 | """ 12 | Borg Singleton pattern as described in Python Patterns, Idioms and Tests. 13 | """ 14 | 15 | _shared_state = None 16 | 17 | def __init__(self): 18 | self.__dict__ = BorgSingleton._shared_state 19 | 20 | 21 | class PluginConfiguration(BorgSingleton): 22 | """ 23 | err-stackstorm shared configuration. 24 | """ 25 | 26 | def __init__(self): 27 | super(BorgSingleton, self).__init__() 28 | 29 | def setup(self, bot_conf): 30 | if not hasattr(bot_conf, "STACKSTORM"): 31 | LOG.critical( 32 | "Missing STACKSTORM configuration in config.py. err-stackstorm must be configured" 33 | " correctly to function. See the err-stackstorm documentation for configuration " 34 | "instructions." 35 | ) 36 | bot_conf.__setattr__("STACKSTORM", {}) 37 | self._configure_prefixes(bot_conf) 38 | self._configure_credentials(bot_conf) 39 | self._configure_rbac_auth(bot_conf) 40 | self._configure_stackstorm(bot_conf) 41 | self.oob_auth_url = bot_conf.STACKSTORM.get("oob_auth_url", "https://localhost:8888/") 42 | self.timer_update = bot_conf.STACKSTORM.get("timer_update", 60) 43 | self.verify_cert = bot_conf.STACKSTORM.get("verify_cert", True) 44 | self.secrets_store = bot_conf.STACKSTORM.get("secrets_store", "cleartext") 45 | self.route_key = bot_conf.STACKSTORM.get("route_key", "errbot") 46 | self.session_ttl = bot_conf.STACKSTORM.get("session_ttl", 3600) 47 | self.user_token_ttl = bot_conf.STACKSTORM.get("user_token_ttl", 86400) 48 | 49 | self.client_cert = bot_conf.STACKSTORM.get("client_cert", None) 50 | self.client_key = bot_conf.STACKSTORM.get("client_key", None) 51 | self.ca_cert = bot_conf.STACKSTORM.get("ca_cert", None) 52 | 53 | def _configure_rbac_auth(self, bot_conf): 54 | self.auth_handler = None 55 | rbac_auth = bot_conf.STACKSTORM.get("rbac_auth", {"standalone": {}}) 56 | for rbac_type in list(rbac_auth.keys()): 57 | self.auth_handler = AuthHandlerFactory.instantiate(rbac_type)( 58 | self, rbac_auth[rbac_type] 59 | ) 60 | if self.auth_handler is None: 61 | LOG.warning( 62 | "Failed to configure RBAC authentication handler. Check the configuration." 63 | ) 64 | return True 65 | 66 | def _configure_prefixes(self, bot_conf): 67 | self.bot_prefix = bot_conf.BOT_PREFIX 68 | self.plugin_prefix = bot_conf.STACKSTORM.get("plugin_prefix", "st2") 69 | # If there is a plugin prefix set we want a space between it and the command, otherwise not 70 | if bot_conf.STACKSTORM.get("plugin_prefix", True): 71 | self.command_prefix = "{} ".format(self.plugin_prefix) 72 | else: 73 | self.command_prefix = "{}".format(self.plugin_prefix) 74 | self.full_prefix = "{}{} ".format(bot_conf.BOT_PREFIX, self.plugin_prefix) 75 | 76 | def _configure_stackstorm(self, bot_conf): 77 | self.api_url = bot_conf.STACKSTORM.get("api_url", "http://localhost:9101/v1") 78 | self.auth_url = bot_conf.STACKSTORM.get("auth_url", "http://localhost:9100/v1") 79 | self.stream_url = bot_conf.STACKSTORM.get("stream_url", "http://localhost:9102/v1") 80 | 81 | def _configure_credentials(self, bot_conf): 82 | self.api_auth = bot_conf.STACKSTORM.get("api_auth", {}) 83 | self.bot_creds = None 84 | for cred_type in ["apikey", "user", "token"]: 85 | c = self.api_auth.get(cred_type) 86 | if c: 87 | if cred_type == "user": 88 | self.bot_creds = CredentialsFactory.instantiate(cred_type)( 89 | c.get("name"), c.get("password") 90 | ) 91 | break 92 | else: 93 | self.bot_creds = CredentialsFactory.instantiate(cred_type)(c) 94 | break 95 | if self.bot_creds is None: 96 | LOG.warning( 97 | "Failed to find any valid st2 credentials for the bot, check configuration file." 98 | ) 99 | -------------------------------------------------------------------------------- /src/err-stackstorm/errst2lib/credentials_adapters.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import abc 3 | import logging 4 | from types import SimpleNamespace 5 | 6 | from requests.auth import HTTPBasicAuth 7 | 8 | LOG = logging.getLogger("errbot.plugin.st2.creds_adapter") 9 | 10 | 11 | class AbstractCredentialsFactory(metaclass=abc.ABCMeta): 12 | @abc.abstractmethod 13 | def __init__(self): 14 | raise NotImplementedError 15 | 16 | @abc.abstractmethod 17 | def instantiate(self, credential_type=None): 18 | raise NotImplementedError 19 | 20 | 21 | class CredentialsFactory(AbstractCredentialsFactory): 22 | def __init__(self): 23 | pass 24 | 25 | @staticmethod 26 | def instantiate(credential_type="user"): 27 | return {"user": St2UserCredentials, "token": St2UserToken, "apikey": St2ApiKey}.get( 28 | credential_type, St2UserCredentials 29 | ) 30 | 31 | 32 | class AbstractCredentials(metaclass=abc.ABCMeta): 33 | @abc.abstractmethod 34 | def __init__(self, username=None, password=None): 35 | raise NotImplementedError 36 | 37 | @abc.abstractmethod 38 | def requests(self): 39 | raise NotImplementedError 40 | 41 | @abc.abstractmethod 42 | def st2client(self): 43 | raise NotImplementedError 44 | 45 | 46 | class St2UserCredentials(AbstractCredentials): 47 | def __init__(self, username=None, password=None): 48 | self.username = username 49 | self.password = password 50 | 51 | def __repr__(self): 52 | return "".join([self.username, " : ", "".join(["*" for c in self.password])]) 53 | 54 | def requests(self, st2_x_auth=False): 55 | # TODO: FIX: Find a cleaner way for requests to produce the header. 56 | headers = ( 57 | HTTPBasicAuth(self.username, self.password) 58 | .__call__(SimpleNamespace(**{"headers": {}})) 59 | .headers 60 | ) 61 | if st2_x_auth: 62 | headers["X-Authenticate"] = headers["Authorization"] 63 | del headers["Authorization"] 64 | return headers 65 | 66 | def st2client(self): 67 | raise NotImplementedError 68 | 69 | 70 | class St2UserToken(AbstractCredentials): 71 | def __init__(self, token=None): 72 | self.token = None 73 | if token: 74 | self.token = token 75 | 76 | def __repr__(self): 77 | return self.token 78 | 79 | def requests(self): 80 | return {"X-Auth-Token": self.token} 81 | 82 | def st2client(self): 83 | return {"token": self.token} 84 | 85 | 86 | class St2ApiKey(AbstractCredentials): 87 | def __init__(self, apikey=None): 88 | self.apikey = None 89 | if apikey: 90 | self.apikey = apikey 91 | 92 | def __repr__(self): 93 | return self.apikey 94 | 95 | def requests(self): 96 | return {"St2-Api-Key": self.apikey} 97 | 98 | def st2client(self): 99 | return {"api_key": self.apikey} 100 | -------------------------------------------------------------------------------- /src/err-stackstorm/errst2lib/enquiry.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import time 4 | 5 | from jsonschema import Draft3Validator # As of May 2022, st2 enquiry uses Draft3 6 | from errbot import BotFlow, BotPlugin, Command, FlowRoot, botcmd, botflow, re_botcmd 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | # https://json-schema.org/draft-07/json-schema-validation.html#rfc.section.6.1.1 11 | 12 | # String values MUST be one of the six primitive types ("null", 13 | # "boolean", "object", "array", "number", or "string"), or "integer" 14 | # which matches any number with a zero fractional part. 15 | 16 | JSON_TYPE = { 17 | "null": None, 18 | "boolean": bool, 19 | "object": dict, 20 | "array": list, 21 | "number": float, # detect '.' use float type otherwise int type. 22 | "string": str, 23 | } 24 | 25 | 26 | class EnquiryManager: 27 | def __init__(self): 28 | """ 29 | enquiries are stored as: 30 | user: 31 | current: enquiry_id 32 | responses: 33 | id: enquiry_obj 34 | """ 35 | self.enquiries = {} 36 | 37 | def get_current_enquiry(self, user_id): 38 | """ 39 | Get the current enquiry id for responses or None if not set. 40 | """ 41 | return self.enquiries.get(user_id, {}).get("current") 42 | 43 | def get(self, user_id, enquiry_id): 44 | self.enquiries.get(user_id)["responses"].get(enquiry_id).next() 45 | 46 | def next(self, user_id): 47 | current = self.enquiries.get(user_id, {}).get("current") 48 | if current: 49 | return self.get(user_id, current) 50 | 51 | def set(self, user_id, enquiry): 52 | if user_id not in self.enquiries: 53 | self.enquiries[user_id] = {"current": None, "responses": {}} 54 | self.enquiries[user_id]["current"] = enquiry 55 | 56 | def purge_expired(self): 57 | purge_list = [] 58 | for user in self.enquiries: 59 | for enquiry in user["responses"]: 60 | if self.enquiries[user]["responses"][enquiry].has_expired: 61 | purge_list.append([user, enquiry]) 62 | for i in purge_list: 63 | user, enquiry = i 64 | del self.enquiries[user]["responses"][enquiry] 65 | 66 | 67 | class Enquiry: 68 | def __init__(self, enquiry, ttl=3600): 69 | """ 70 | The Enquiry class wraps the St2 API response in a Python Object. The Python object 71 | tracks the answers provided for the specific enquiry and maintains a time to live for 72 | answers to be considered abandonned. 73 | 74 | enquiry: the stackstorm enquiry API response object in JSON form. 75 | ttl: seconds to consider the enquiry should remain. 76 | """ 77 | self.validator = None 78 | 79 | self.enquiry_data = json.loads(enquiry) 80 | self.answers = {} 81 | self.expiration = time.time() + ttl 82 | 83 | @classmethod 84 | def get_associated_data(cls): 85 | """ 86 | The enquiry data is insufficent to identify it's association 87 | with a specific workflow. The get_associated_data queries the 88 | associated execution_id and then the associated workflow. to 89 | collect descriptions 90 | """ 91 | raise NotImplementedError 92 | 93 | @property 94 | def id(self): 95 | return self.enquiry_data["id"] 96 | 97 | def next(self): 98 | for k in self.enquiry_data["schema"]["properties"]: 99 | if k not in self.answers: 100 | return self.enquiry_data["schema"]["properties"][k].get("description") 101 | else: 102 | return "All questions have been answered, use enquiry submit." 103 | 104 | @property 105 | def has_expired(self): 106 | return time.time() > self.expiration 107 | 108 | def response(self, num, answer): 109 | """ 110 | response: Answer a specific question in the enquiry. 111 | num: int: the question to be answered (order in the query) 112 | answer: any: the answer that conforms with the enquiry schema 113 | """ 114 | if self.answers: 115 | return "finished" 116 | else: 117 | self.answers["a"] = 1 118 | return "Is this ok?" 119 | 120 | def check_answer(self, num, answer): 121 | """ 122 | compare the answer data type with the schema 123 | """ 124 | raise NotImplementedError 125 | 126 | def check_schema(self): 127 | """ 128 | compare the answer data type with the schema 129 | """ 130 | self.validator = Draft3Validator(json.loads(enquiry)["schema"]) 131 | try: 132 | self.validator.check_schema(self.enquiry["schema"]) 133 | except jsonschema.exceptions.SchemaError as err: 134 | log.exception(err) 135 | return False 136 | return True 137 | -------------------------------------------------------------------------------- /src/err-stackstorm/errst2lib/errors.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | 4 | class Error(Exception): 5 | def __str__(self): 6 | return self.message 7 | 8 | 9 | class SessionExpiredError(Error): 10 | def __init__(self, message="Session has expired."): 11 | super(SessionExpiredError, self).__init__() 12 | self.message = message 13 | 14 | 15 | class SessionInvalidError(Error): 16 | def __init__(self, message="Session is invalid."): 17 | super(SessionInvalidError, self).__init__() 18 | self.message = message 19 | 20 | 21 | class SessionConsumedError(Error): 22 | def __init__(self, message="Session has been consumed."): 23 | super(SessionConsumedError, self).__init__() 24 | self.message = message 25 | 26 | 27 | class SessionExistsError(Error): 28 | def __init__(self, message="Session already exists."): 29 | super(SessionExistsError, self).__init__() 30 | self.message = message 31 | -------------------------------------------------------------------------------- /src/err-stackstorm/errst2lib/session.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import hashlib 3 | import logging 4 | import string 5 | import uuid 6 | from datetime import datetime as dt 7 | from random import SystemRandom 8 | 9 | from errst2lib.errors import SessionConsumedError, SessionExpiredError 10 | 11 | LOG = logging.getLogger("errbot.plugin.st2.session") 12 | 13 | 14 | def generate_password(length=8): 15 | rnd = SystemRandom() 16 | if length > 255: 17 | length = 255 18 | return "".join([rnd.choice(string.hexdigits) for _ in range(length)]) 19 | 20 | 21 | class Session(object): 22 | def __init__(self, user_id, user_secret, session_ttl=3600): 23 | self.bot_secret = None 24 | self.user_id = user_id 25 | self._is_sealed = True 26 | self.session_id = uuid.uuid4() 27 | self.create_date = int(dt.now().timestamp()) 28 | self.modified_date = self.create_date 29 | self.ttl_in_seconds = session_ttl 30 | self._hashed_secret = self.hash_secret(user_secret) 31 | del user_secret 32 | 33 | def is_expired(self): 34 | """ 35 | Returns False if both create and modified timestamps have exceeded the ttl. 36 | """ 37 | now = int(dt.now().timestamp()) 38 | modified_expiry = self.modified_date + self.ttl_in_seconds 39 | if modified_expiry < now: 40 | raise SessionExpiredError 41 | return False 42 | 43 | def attributes(self): 44 | return { 45 | "UserID": self.user_id, 46 | "IsSealed": self._is_sealed, 47 | "SessionID": self.session_id, 48 | "CreationDate": str(dt.fromtimestamp(self.create_date)), 49 | "ModifiedDate": str(dt.fromtimestamp(self.modified_date)), 50 | "ExpiryDate": str(dt.fromtimestamp(self.modified_date + self.ttl_in_seconds)), 51 | } 52 | 53 | def __repr__(self): 54 | return " ".join( 55 | [ 56 | "UserID: {},".format(str(self.user_id)), 57 | "Is Sealed: {},".format(str(self._is_sealed)), 58 | "SessionID: {},".format(str(self.session_id)), 59 | "Creation Date: {},".format(str(dt.fromtimestamp(self.create_date))), 60 | "Modified Date: {},".format(str(dt.fromtimestamp(self.modified_date))), 61 | "Expiry Date: {}".format( 62 | str(dt.fromtimestamp(self.modified_date + self.ttl_in_seconds)) 63 | ), 64 | ] 65 | ) 66 | 67 | def unseal(self): 68 | """ 69 | Mark the session as being consumed. Returns true if the session was available to be 70 | consumed or raises SessionConsumedError if the session has already been marked as consumed. 71 | """ 72 | self.is_expired() 73 | if self._is_sealed: 74 | self._is_sealed = False 75 | else: 76 | raise SessionConsumedError 77 | return True 78 | 79 | def is_sealed(self): 80 | """ 81 | Query the state of the one time use flag. 82 | Returns True if the session has not been consumed or False if the session has already been 83 | consumed. 84 | """ 85 | self.is_expired() 86 | return self._is_sealed 87 | 88 | def id(self): 89 | """ 90 | Return the UUID for the session. 91 | """ 92 | return str(self.session_id) 93 | 94 | def ttl(self, ttl=None): 95 | """ 96 | Get/Set the time to live for the session. 97 | param: ttl[int] The number of seconds the session should remain valid since creation or 98 | modification. 99 | Returns the number of seconds the ttl has been set to if no agrument is provided otherwise 100 | the ttl is set to the number of seconds provided to the ttl argument. 101 | """ 102 | self.is_expired() 103 | if ttl is None: 104 | return self.ttl_in_seconds 105 | 106 | if isinstance(ttl, int): 107 | self.ttl_in_seconds = ttl 108 | self.modified_date = int(dt.now().timestamp()) 109 | else: 110 | LOG.warning("session ttl must be an integer type, got '{}'".format(ttl)) 111 | 112 | def hash_secret(self, user_secret): 113 | """ 114 | Generate a unique token by hashing a random bot secret with the user secrets. 115 | param: user_secret[string] - The users secret provided in the chat backend. 116 | """ 117 | self.is_expired() 118 | if self.bot_secret is None: 119 | self.bot_secret = generate_password(8) 120 | h = hashlib.sha256() 121 | h.update(bytes(user_secret, "utf-8")) 122 | del user_secret 123 | h.update(bytes(self.bot_secret, "utf-8")) 124 | return h.hexdigest() 125 | 126 | def match_secret(self, user_secret): 127 | """ 128 | Compare a secret with the session's hashed secret. 129 | param: user_secret[string] the secret to compare. 130 | Return True if the user_secret hash has matches the session hash or False if it does not. 131 | """ 132 | self.is_expired() 133 | return self._hashed_secret == self.hash_secret(user_secret) 134 | -------------------------------------------------------------------------------- /src/err-stackstorm/errst2lib/session_manager.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import logging 3 | 4 | from errst2lib.errors import SessionExistsError, SessionInvalidError 5 | from errst2lib.session import Session 6 | from errst2lib.store_adapters import SessionStore, StoreAdapterFactory 7 | 8 | LOG = logging.getLogger("errbot.plugin.st2.session_mgr") 9 | 10 | 11 | class SessionManager(object): 12 | def __init__(self, cfg): 13 | self.cfg = cfg 14 | self.store = SessionStore() 15 | self.secure_store = StoreAdapterFactory.instantiate(cfg.secrets_store)() 16 | self.secure_store.setup() 17 | 18 | def get_by_userid(self, user_id): 19 | """ 20 | Fetch information from the store by user_id. 21 | param: user_id[string] A string uniquely identifying the chat user. 22 | """ 23 | session = self.store.get_by_userid(user_id) 24 | if session is False: 25 | raise SessionInvalidError 26 | return session 27 | 28 | def get_by_uuid(self, _uuid): 29 | """ 30 | Fetch information related to a session by its UUID. 31 | If a session doesn't exist, False is returned. 32 | """ 33 | session = self.store.get_by_uuid(_uuid) 34 | if session is False: 35 | raise SessionInvalidError 36 | return session 37 | 38 | def create(self, user_id, user_secret, session_ttl): 39 | """ 40 | param: user_id - Chat user unique identifier. 41 | param: user_secret - A pseudo-secret word provided by 42 | the user and used as part of the UUID hashing process. 43 | """ 44 | # Don't create a new session if one already exists. 45 | if self.exists(user_id): 46 | raise SessionExistsError 47 | session = Session(user_id, user_secret, session_ttl) 48 | # Store the session. 49 | self.store.put(session) 50 | return session 51 | 52 | def delete(self, user_id): 53 | """ 54 | Remove a session from the manager 55 | """ 56 | session = self.get_by_userid(user_id) 57 | if session: 58 | self.secure_store.delete(session.id()) 59 | self.store.delete(user_id) 60 | 61 | def list_sessions(self): 62 | """ 63 | List all available sessions. 64 | """ 65 | return self.store.list() 66 | 67 | def update(self, session): 68 | raise NotImplementedError 69 | 70 | def exists(self, user_id): 71 | return self.store.get_by_userid(user_id) is not False 72 | 73 | def put_secret(self, session_id, secret): 74 | self.secure_store.set(session_id, secret) 75 | return True 76 | 77 | def get_secret(self, session_id): 78 | return self.secure_store.get(session_id) 79 | -------------------------------------------------------------------------------- /src/err-stackstorm/errst2lib/stackstorm_api.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import json 3 | import logging 4 | import time 5 | import traceback 6 | 7 | import requests 8 | import sseclient 9 | import urllib3 10 | from requests.exceptions import HTTPError 11 | 12 | LOG = logging.getLogger("errbot.plugin.st2.st2_api") 13 | 14 | 15 | class Result(object): 16 | def __init__(self, return_code=None, message=None): 17 | self.return_code = return_code 18 | self.message = message 19 | 20 | def OK(self, message): 21 | self.return_code = 0 22 | self.message = message 23 | 24 | def error(self, return_code, message): 25 | self.return_code = return_code 26 | self.message = message 27 | 28 | 29 | class StackStormAPI(object): 30 | stream_backoff = 10 31 | authenticate_backoff = 10 32 | http_timeout = 10 33 | 34 | def __init__(self, cfg, accessctl): 35 | self.cfg = cfg 36 | self.accessctl = accessctl 37 | 38 | if self.cfg.verify_cert is False: 39 | urllib3.disable_warnings() 40 | 41 | def refresh_bot_credentials(self): 42 | LOG.warning("Bot credentials re-authentication required.") 43 | session_id = self.accessctl.get_session(self.accessctl.bot.internal_identity) 44 | self.accessctl.bot.reauthenticate_bot_credentials(session_id) 45 | 46 | def action_get(self, action_id): 47 | raise NotImplementedError 48 | 49 | def workflow_get(self, action_id): 50 | raise NotImplementedError 51 | 52 | def enquiry_list(self, st2_creds=None): 53 | """ 54 | curl -X GET -H 'X-Auth-Token: X' 'http://127.0.0.1:9101/v1/inquiries/?limit=50' 55 | """ 56 | url = f"{self.cfg.api_url}/inquiries/" 57 | params = {} 58 | headers = st2_creds.requests() 59 | return requests.get( 60 | url, 61 | headers=headers, 62 | params=params, 63 | timeout=StackStormAPI.http_timeout, 64 | verify=self.cfg.verify_cert, 65 | ) 66 | 67 | def enquiry_get(self, enquiry_id, st2_creds=None): 68 | """ 69 | StackStorm currently uses the draft4 jsonschema validator. 70 | 71 | Fetch the contents of an inquiry given its id. 72 | 73 | { 74 | "id": "60a7c8876d573fae8028be34", 75 | "route": "slack_query", 76 | "ttl": 1440, 77 | "users": [], 78 | "roles": [], 79 | "schema": { 80 | "type" : "object", 81 | "properties": { 82 | "password": { 83 | "type": "string", 84 | "description": "Enter your not so secret password", 85 | "required": true 86 | } 87 | } 88 | } 89 | } 90 | curl -X GET -H 'User-Agent: python-requests/2.25.1' 91 | -H 'Accept-Encoding: gzip, deflate' 92 | -H 'Accept: */*' 93 | -H 'Connection: keep-alive' 94 | -H 'X-Auth-Token: b678fac0557f4fc7893e82d31a615942' 95 | http://127.0.0.1:9101/v1/inquiries/60a81cee6d573fae8028be84 96 | 97 | { 98 | "id": "60a81cee6d573fae8028be84", 99 | "route": "slack_query", 100 | "ttl": 1440, 101 | "users": [], 102 | } 103 | } 104 | } 105 | """ 106 | 107 | url = f"{self.cfg.api_url}/inquiries/{enquiry_id}" 108 | params = {} 109 | headers = st2_creds.requests() 110 | response = requests.get( 111 | url, 112 | headers=headers, 113 | params=params, 114 | timeout=StackStormAPI.http_timeout, 115 | verify=self.cfg.verify_cert, 116 | ) 117 | 118 | if response.status_code == requests.codes.ok: 119 | return response.json() 120 | elif response.status_code == 401: 121 | self.refresh_bot_credentials() 122 | return "Attempted to access API without authentication. " 123 | "Try again or fix the bot authorisation." 124 | else: 125 | return response.json() 126 | 127 | def actionalias_help(self, pack=None, filter=None, limit=None, offset=None, st2_creds=None): 128 | """ 129 | Call StackStorm API for action alias help. 130 | """ 131 | # curl -v -H "X-Auth-Token: $ST2_AUTH_TOKEN" 132 | # -H 'Content-Type: application/json' 133 | # -XGET localhost:9101/v1/actionalias/help -d '{}' 134 | 135 | url = "/".join([self.cfg.api_url, "actionalias/help"]) 136 | 137 | params = {} 138 | if pack is not None: 139 | params["pack"] = pack 140 | if filter is not None: 141 | params["filter"] = filter 142 | if limit is not None: 143 | params["limit"] = limit 144 | if offset is not None: 145 | params["offset"] = offset 146 | 147 | headers = st2_creds.requests() 148 | response = requests.get( 149 | url, 150 | headers=headers, 151 | params=params, 152 | timeout=StackStormAPI.http_timeout, 153 | verify=self.cfg.verify_cert, 154 | ) 155 | if response.status_code == requests.codes.ok: 156 | return response.json().get("helpstrings", []) 157 | elif response.status_code == 401: 158 | self.refresh_bot_credentials() 159 | else: 160 | response.raise_for_status() 161 | 162 | def match(self, text, st2token): 163 | headers = st2token.requests() 164 | url = "/".join([self.cfg.api_url, "actionalias/match"]) 165 | 166 | payload = json.dumps({"command": text}) 167 | headers["Content-Type"] = "application/json" 168 | 169 | result = Result() 170 | try: 171 | response = requests.post( 172 | url, 173 | headers=headers, 174 | data=payload, 175 | timeout=StackStormAPI.http_timeout, 176 | verify=self.cfg.verify_cert, 177 | ) 178 | if response.status_code == 200: 179 | result.OK(response.json()) 180 | elif response.status_code == 400: 181 | result.error( 182 | 1, 183 | "st2 command '{}' not found. View available commands with {}{}help.".format( 184 | text, self.cfg.bot_prefix, self.cfg.plugin_prefix 185 | ), 186 | ) 187 | LOG.error(result.message) 188 | else: 189 | response.raise_for_status() 190 | except HTTPError as e: 191 | result.error(2, "HTTPError {}".format(str(e))) 192 | LOG.error(result.message) 193 | except Exception as e: 194 | result.error(3, "Unexpected error {}".format(e)) 195 | LOG.error(result.message) 196 | return result 197 | 198 | def execute_actionalias(self, msg, chat_user, st2token): 199 | """ 200 | @msg: errbot message. 201 | @chat_user: the chat provider user/channel to pass to StackStorm for the execution 202 | result response. 203 | @st2token: The st2 api token/key to be used when submitting the action-alias execution. 204 | """ 205 | headers = st2token.requests() 206 | 207 | url = "/".join([self.cfg.api_url, "aliasexecution/match_and_execute"]) 208 | 209 | payload = {"command": msg.body, "user": chat_user, "notification_route": self.cfg.route_key} 210 | 211 | if msg.is_direct is False: 212 | payload["source_channel"] = str(msg.to) 213 | payload["notification_channel"] = str(msg.to) 214 | else: 215 | payload["source_channel"] = str(msg.frm) 216 | payload["notification_channel"] = str(msg.frm) 217 | 218 | msg = "" 219 | try: 220 | response = requests.post( 221 | url, 222 | headers=headers, 223 | json=payload, 224 | timeout=StackStormAPI.http_timeout, 225 | verify=self.cfg.verify_cert, 226 | ) 227 | 228 | if response.status_code in [201, 400]: 229 | msg = response.json() 230 | else: 231 | msg = response.text 232 | except Exception as e: 233 | msg = "Error executing action-alias: {}".format(str(e)) 234 | return msg 235 | 236 | def st2stream_listener(self, callback=None, bot_identity=None): 237 | """ 238 | Listen for events passing through the stackstorm bus 239 | """ 240 | LOG.info("*** Start stream listener ***") 241 | 242 | def listener(callback=None, bot_identity=None): 243 | 244 | token = self.accessctl.get_token_by_userid(bot_identity) 245 | if not token: 246 | self.refresh_bot_credentials() 247 | token = self.accessctl.get_token_by_userid(bot_identity) 248 | if not token: 249 | LOG.debug( 250 | "[STREAM] Failed to get a valid token for the bot." 251 | "Reconnecting to stream api." 252 | ) 253 | raise ValueError("Bot token is not valid for Stream API.") 254 | 255 | stream_kwargs = {"headers": token.requests(), "verify": self.cfg.verify_cert} 256 | 257 | stream_url = "".join([self.cfg.stream_url, "/stream"]) 258 | 259 | stream = sseclient.SSEClient(stream_url, **stream_kwargs) 260 | for event in stream: 261 | if event.event == "st2.announcement__{}".format(self.cfg.route_key): 262 | LOG.debug( 263 | "*** Errbot announcement event detected! ***\n{}\n{}\n".format( 264 | event.dump(), stream 265 | ) 266 | ) 267 | data = json.loads(event.data) 268 | if data.get("context") is not None: 269 | LOG.info("Inquiry payload detected, looking up inquery data.") 270 | else: 271 | p = data["payload"] 272 | callback( 273 | p.get("whisper"), 274 | p.get("message"), 275 | p.get("user"), 276 | p.get("channel"), 277 | p.get("extra"), 278 | ) 279 | # Test for shutdown after event to avoid losing messages. 280 | if self.accessctl.bot.run_listener is False: 281 | break 282 | 283 | StackStormAPI.stream_backoff = 10 284 | while self.accessctl.bot.run_listener: 285 | try: 286 | self.refresh_bot_credentials() 287 | listener(callback, bot_identity) 288 | except Exception as err: 289 | LOG.critical( 290 | "St2 stream listener - An error occurred: {} {}. " 291 | "Backing off {} seconds.".format(type(err), err, StackStormAPI.stream_backoff) 292 | ) 293 | traceback.print_exc() 294 | time.sleep(StackStormAPI.stream_backoff) 295 | LOG.info("*** Exit stream listener ***") 296 | -------------------------------------------------------------------------------- /src/err-stackstorm/errst2lib/store_adapters.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import abc 3 | import logging 4 | 5 | LOG = logging.getLogger(__name__) 6 | 7 | 8 | class AbstractStoreAdapterFactory(metaclass=abc.ABCMeta): 9 | @abc.abstractmethod 10 | def instantiate(store_type): 11 | raise NotImplementedError 12 | 13 | 14 | class StoreAdapterFactory(AbstractStoreAdapterFactory): 15 | @staticmethod 16 | def instantiate(store_type): 17 | LOG.debug("Create secret store for '{}'".format(store_type)) 18 | return {"cleartext": ClearTextStoreAdapter}.get(store_type, ClearTextStoreAdapter) 19 | 20 | 21 | class AbstractStoreAdapter(metaclass=abc.ABCMeta): 22 | @abc.abstractmethod 23 | def setup(self, *args, **kwargs): 24 | raise NotImplementedError 25 | 26 | @abc.abstractmethod 27 | def set(self, name, secret, namespace): 28 | raise NotImplementedError 29 | 30 | @abc.abstractmethod 31 | def get(self, name, namespace): 32 | raise NotImplementedError 33 | 34 | @abc.abstractmethod 35 | def teardown(self, user): 36 | raise NotImplementedError 37 | 38 | 39 | class ClearTextStoreAdapter(AbstractStoreAdapter): 40 | """ 41 | The clear text store adapter doesn't encrypt data in memory, but doesn't persist it to disk 42 | either. If more secure methods are required to operate, open an issue requesting a new feature. 43 | """ 44 | 45 | def __init__(self): 46 | self.associations = {} 47 | 48 | def __str__(self): 49 | return str(self.associations) 50 | 51 | def setup(self): 52 | pass 53 | 54 | def set(self, name, secret, namespace=""): 55 | self.associations[name] = secret 56 | return True 57 | 58 | def get(self, name, namespace=""): 59 | return self.associations.get(name) 60 | 61 | def delete(self, name, namespace=""): 62 | if name in self.associations: 63 | del self.associations[name] 64 | 65 | def teardown(self): 66 | pass 67 | 68 | 69 | class SessionStore(object): 70 | def __init__(self): 71 | """ 72 | Sessions are stored by userid with a lookup index for uuid's to user_ids. 73 | """ 74 | self.memory = {} 75 | self.id_to_user_map = {} 76 | 77 | def list(self): 78 | """ 79 | Return a list of string representation of session. 80 | """ 81 | return [self.memory[k] for k in self.memory.keys()] 82 | 83 | def get_by_userid(self, user_id): 84 | """ 85 | Get information by user_id. 86 | """ 87 | return self.memory.get(user_id, False) 88 | 89 | def put(self, session): 90 | """ 91 | Put a new session in the store using the user_id as the key 92 | and create a reverse mapping between the user_id and the session_id. 93 | """ 94 | self.memory[session.user_id] = session 95 | self.id_to_user_map[session.id()] = session.user_id 96 | 97 | def delete(self, user_id): 98 | """ 99 | Delete a session by user_id. Delete the reverse mapping 100 | if it exists. 101 | """ 102 | session = self.memory.get(user_id, False) 103 | if session: 104 | if session.id() in self.id_to_user_map: 105 | del self.id_to_user_map[session.id()] 106 | del self.memory[user_id] 107 | else: 108 | LOG.warning("Failed to delete user_id {} session - Not found.".format(user_id)) 109 | 110 | def put_by_id(self, session_id, session): 111 | """ 112 | Put a session in the store using the session id. 113 | """ 114 | if session.user_id in self.memory: 115 | self.id_to_user_map[session_id] = session.user_id 116 | 117 | def get_by_uuid(self, session_id): 118 | user_id = self.id_to_user_map.get(session_id, False) 119 | session = self.memory.get(user_id, False) 120 | if session is False: 121 | LOG.debug("Error: Session id '{}' points to a missing session.".format("")) 122 | return session 123 | -------------------------------------------------------------------------------- /src/err-stackstorm/errst2lib/version.py: -------------------------------------------------------------------------------- 1 | ERR_STACKSTORM_VERSION = "2.1.4" 2 | -------------------------------------------------------------------------------- /src/err-stackstorm/html/errbot_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nzlosh/err-stackstorm/32fa23a5821bcbccd05c2e65e89b1aaec9402c9e/src/err-stackstorm/html/errbot_icon.png -------------------------------------------------------------------------------- /src/err-stackstorm/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 175 | 176 | 177 | 178 |
179 |
180 | 181 |
182 |
183 | 184 | 185 | 186 |
187 |
Err-stackstorm Authentication
188 |
Associate your StackStorm token/key with your chat account.
189 |
190 | 191 |
192 |
193 |
194 | 195 | Username/password 196 |
197 |
198 |
199 |
200 | 201 |
202 |
203 | 204 |
205 |
206 | 207 |
208 | 209 |
210 |
211 |
212 | 213 | User Token 214 |
215 |
216 |
217 |
218 | 219 |
220 |
221 | 222 |
223 | 224 |
225 |
226 |
227 | 228 | API Key 229 |
230 |
231 |
232 |
233 | 234 |
235 |
236 | 237 |
238 | 239 |
240 |
241 |
242 | 243 |
244 |
245 |
246 | 249 |

Waiting for user credential submission.

250 |
251 |
252 |
253 | 254 |
255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /src/err-stackstorm/html/st2_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nzlosh/err-stackstorm/32fa23a5821bcbccd05c2e65e89b1aaec9402c9e/src/err-stackstorm/html/st2_logo.png -------------------------------------------------------------------------------- /src/err-stackstorm/st2.plug: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = St2 3 | Module = st2 4 | 5 | [Documentation] 6 | Description = StackStorm chatops plugin for Errbot. 7 | 8 | [Python] 9 | Version = 3 10 | -------------------------------------------------------------------------------- /tests/disable_enquiry.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import time 3 | 4 | import pytest 5 | 6 | from errst2lib.enquiry import EnquiryManager, Enquiry 7 | 8 | pytest_plugins = ["errbot.backends.test"] 9 | extra_plugin_dir = "." 10 | 11 | 12 | def test_enquiry(): 13 | """ 14 | Enquiry class tests 15 | """ 16 | user_id = "test_id" 17 | 18 | enquiry_data = { 19 | "id": "60a7c8876d573fae8028be34", 20 | "route": "slack_query", 21 | "ttl": 1440, 22 | "users": [], 23 | "roles": [], 24 | "schema": { 25 | "type": "object", 26 | "properties": { 27 | "password": { 28 | "type": "string", 29 | "description": "Run this workflow (yes/no)", 30 | "required": True, 31 | }, 32 | }, 33 | }, 34 | } 35 | enquiry_id = enquiry_data["id"] 36 | 37 | enquiry = Enquiry(enquiry_data) 38 | 39 | # to be completed. 40 | 41 | 42 | if __name__ == "__main__": 43 | print("Run with pytest") 44 | exit(1) 45 | -------------------------------------------------------------------------------- /tests/test_access_control.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import time 3 | 4 | import pytest 5 | from mock import Mock 6 | 7 | from errst2lib.credentials_adapters import CredentialsFactory 8 | from errst2lib.errors import ( 9 | SessionConsumedError, 10 | SessionExistsError, 11 | SessionExpiredError, 12 | SessionInvalidError, 13 | ) 14 | from errst2lib.session import Session 15 | from errst2lib.session_manager import SessionManager 16 | from errst2lib.store_adapters import ClearTextStoreAdapter, StoreAdapterFactory 17 | 18 | pytest_plugins = ["errbot.backends.test"] 19 | extra_plugin_dir = "." 20 | 21 | 22 | def test_access_control(): 23 | """ 24 | Access Control 25 | """ 26 | 27 | 28 | # access_control 29 | # create a session 30 | # get a session by session_id 31 | # get a session by chat_user 32 | # delete a session by session_id 33 | # delete a session by chat_user 34 | # list sessions 35 | # set an st2 token by chat_user 36 | # get an st2 token by session_id 37 | 38 | 39 | if __name__ == "__main__": 40 | print("Run with pytest") 41 | exit(1) 42 | -------------------------------------------------------------------------------- /tests/test_auth_handlers.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | from errst2lib.authentication_handler import ClientSideAuthHandler, StandaloneAuthHandler 4 | from errst2lib.credentials_adapters import ( 5 | CredentialsFactory, 6 | St2UserCredentials, 7 | St2UserToken, 8 | St2ApiKey, 9 | ) 10 | 11 | pytest_plugins = ["errbot.backends.test"] 12 | extra_plugin_dir = "." 13 | 14 | 15 | def test_credentials_manager(): 16 | """ 17 | Credentials manager 18 | """ 19 | username = "peter.parker" 20 | password = "stickyweb" 21 | token = "abcdef1234" 22 | apikey = "1234abcdef" 23 | 24 | # request user 25 | user_creds = CredentialsFactory.instantiate("user")(username, password) 26 | assert isinstance(user_creds, St2UserCredentials) 27 | assert user_creds.requests() == {"Authorization": "Basic cGV0ZXIucGFya2VyOnN0aWNreXdlYg=="} 28 | 29 | # requests token 30 | token_creds = CredentialsFactory.instantiate("token")(token) 31 | assert isinstance(token_creds, St2UserToken) 32 | assert token_creds.requests() == {"X-Auth-Token": token} 33 | 34 | # requests api key 35 | apikey_creds = CredentialsFactory.instantiate("apikey")(apikey) 36 | assert isinstance(apikey_creds, St2ApiKey) 37 | assert apikey_creds.requests() == {"St2-Api-Key": apikey} 38 | 39 | 40 | if __name__ == "__main__": 41 | print("Run with python -m pytest") 42 | exit(1) 43 | -------------------------------------------------------------------------------- /tests/test_bot_commands.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import time 3 | 4 | import pytest 5 | from mock import Mock 6 | 7 | from errst2lib.credentials_adapters import CredentialsFactory 8 | from errst2lib.errors import ( 9 | SessionConsumedError, 10 | SessionExistsError, 11 | SessionExpiredError, 12 | SessionInvalidError, 13 | ) 14 | from errst2lib.session import Session 15 | from errst2lib.session_manager import SessionManager 16 | from errst2lib.store_adapters import ClearTextStoreAdapter, StoreAdapterFactory 17 | 18 | pytest_plugins = ["errbot.backends.test"] 19 | extra_plugin_dir = "." 20 | 21 | 22 | def test_bot_commands(): 23 | """ 24 | Bot Commands 25 | """ 26 | 27 | 28 | # bot_commands 29 | # display help 30 | # match and execute action-alias 31 | 32 | if __name__ == "__main__": 33 | print("Run with pytest") 34 | exit(1) 35 | -------------------------------------------------------------------------------- /tests/test_chat_adapters.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | from mock import Mock 3 | 4 | from errst2lib.chat_adapters import ( 5 | ChatAdapterFactory, 6 | GenericChatAdapter, 7 | MattermostChatAdapter, 8 | SlackChatAdapter, 9 | XMPPChatAdapter, 10 | ) 11 | 12 | pytest_plugins = ["errbot.backends.test"] 13 | extra_plugin_dir = "." 14 | 15 | 16 | def test_chat_adapters(): 17 | """ 18 | Chat Adapters 19 | """ 20 | slack = ChatAdapterFactory.instance("slack")(Mock()) 21 | assert isinstance(slack, SlackChatAdapter) 22 | 23 | mattermost = ChatAdapterFactory.instance("mattermost")(Mock()) 24 | assert isinstance(mattermost, MattermostChatAdapter) 25 | 26 | xmpp = ChatAdapterFactory.instance("xmpp")(Mock()) 27 | assert isinstance(xmpp, XMPPChatAdapter) 28 | 29 | generic = ChatAdapterFactory.instance("generic")(Mock()) 30 | assert isinstance(generic, GenericChatAdapter) 31 | 32 | 33 | if __name__ == "__main__": 34 | print("Run with pytest") 35 | exit(1) 36 | -------------------------------------------------------------------------------- /tests/test_credentials_manager.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | from errst2lib.credentials_adapters import ( 3 | CredentialsFactory, 4 | St2ApiKey, 5 | St2UserCredentials, 6 | St2UserToken, 7 | ) 8 | 9 | pytest_plugins = ["errbot.backends.test"] 10 | extra_plugin_dir = "." 11 | 12 | 13 | def test_credentials_manager(): 14 | """ 15 | Credentials manager 16 | """ 17 | username = "peter.parker" 18 | password = "stickyweb" 19 | token = "abcdef1234" 20 | apikey = "1234abcdef" 21 | 22 | # request user 23 | user_creds = CredentialsFactory.instantiate("user")(username, password) 24 | assert isinstance(user_creds, St2UserCredentials) 25 | assert user_creds.requests() == {"Authorization": "Basic cGV0ZXIucGFya2VyOnN0aWNreXdlYg=="} 26 | 27 | # requests token 28 | token_creds = CredentialsFactory.instantiate("token")(token) 29 | assert isinstance(token_creds, St2UserToken) 30 | assert token_creds.requests() == {"X-Auth-Token": token} 31 | 32 | # requests api key 33 | apikey_creds = CredentialsFactory.instantiate("apikey")(apikey) 34 | assert isinstance(apikey_creds, St2ApiKey) 35 | assert apikey_creds.requests() == {"St2-Api-Key": apikey} 36 | 37 | 38 | if __name__ == "__main__": 39 | print("Run with python -m pytest") 40 | exit(1) 41 | -------------------------------------------------------------------------------- /tests/test_secret_store.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | from errst2lib.store_adapters import ClearTextStoreAdapter, StoreAdapterFactory 3 | 4 | pytest_plugins = ["errbot.backends.test"] 5 | extra_plugin_dir = "." 6 | 7 | 8 | def test_secret_store(): 9 | """ 10 | Clear Text Store backend. 11 | """ 12 | st2_token = "123456-abcdef-123456" 13 | 14 | cleartext = StoreAdapterFactory.instantiate("cleartext")() 15 | assert isinstance(cleartext, ClearTextStoreAdapter) 16 | 17 | # set a secret 18 | cleartext.set("token", st2_token) 19 | 20 | # get a secret 21 | assert st2_token == cleartext.get("token") 22 | 23 | # delete a secret 24 | cleartext.delete("token") 25 | assert None is cleartext.get("token") 26 | 27 | 28 | if __name__ == "__main__": 29 | print("Run with python -m pytest") 30 | exit(1) 31 | -------------------------------------------------------------------------------- /tests/test_session.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import time 3 | 4 | import pytest 5 | 6 | from errst2lib.errors import SessionExpiredError 7 | from errst2lib.session import Session 8 | 9 | pytest_plugins = ["errbot.backends.test"] 10 | extra_plugin_dir = "." 11 | 12 | 13 | def test_session(): 14 | """ 15 | Session class tests 16 | """ 17 | user_id = "test_id" 18 | secret = "test_secret" 19 | session = Session(user_id, secret) 20 | 21 | # Session not expired 22 | assert False is session.is_expired() 23 | 24 | # Unseal a session 25 | assert True is session.is_sealed() 26 | session.unseal() 27 | assert False is session.is_sealed() 28 | 29 | # Time-to-live 30 | assert 3600 == session.ttl() 31 | session.ttl(300) 32 | assert 300 == session.ttl() 33 | 34 | # Secret 35 | assert True is session.match_secret(secret) 36 | 37 | # Expired session 38 | session.ttl(1) 39 | time.sleep(2) 40 | with pytest.raises(SessionExpiredError): 41 | session.is_expired() 42 | 43 | 44 | if __name__ == "__main__": 45 | print("Run with pytest") 46 | exit(1) 47 | -------------------------------------------------------------------------------- /tests/test_session_manager.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import pytest 3 | from mock import Mock 4 | 5 | from errst2lib.errors import SessionExistsError, SessionInvalidError 6 | from errst2lib.session_manager import SessionManager 7 | from errst2lib.store_adapters import ClearTextStoreAdapter 8 | 9 | pytest_plugins = ["errbot.backends.test"] 10 | extra_plugin_dir = "." 11 | 12 | 13 | def test_session_manager(): 14 | """ 15 | SessionManager class tests 16 | """ 17 | cfg = Mock() 18 | cfg.secrets_store = "cleartext" 19 | 20 | user_id = "test%user" 21 | user_secret = "secret_for_test" 22 | user_token = "1234567890-0987654321" 23 | session_ttl = 5000 24 | 25 | session_manager = SessionManager(cfg) 26 | 27 | # Have requested backend 28 | assert isinstance(session_manager.secure_store, ClearTextStoreAdapter) 29 | 30 | # create a new user 31 | s = session_manager.create(user_id, user_secret, session_ttl) 32 | assert s.user_id == user_id 33 | 34 | # 1 session in list 35 | assert len(session_manager.list_sessions()) == 1 36 | 37 | # create same user raise exception 38 | with pytest.raises(SessionExistsError): 39 | session_manager.create(user_id, user_secret, session_ttl) 40 | 41 | # get an existing session 42 | s1 = session_manager.get_by_uuid(s.id()) 43 | assert s1.id() == s.id() 44 | 45 | # get a non-existent session 46 | with pytest.raises(SessionInvalidError): 47 | session_manager.get_by_uuid(s.id() + "no_uuid") 48 | 49 | # get an existing session by user 50 | s2 = session_manager.get_by_userid(user_id) 51 | assert s2.id() == s.id() 52 | 53 | # get a non-existent session by user 54 | with pytest.raises(SessionInvalidError): 55 | session_manager.get_by_userid(user_id + "no_user") 56 | 57 | # confirm existence 58 | assert session_manager.exists(user_id) is True 59 | 60 | # confirm non-existence 61 | assert session_manager.exists(user_id + "no_user") is False 62 | 63 | # Set value in secret store 64 | assert session_manager.put_secret(s.id(), user_token) is True 65 | 66 | # Get value in secret store 67 | assert session_manager.get_secret(s.id()) == user_token 68 | 69 | 70 | if __name__ == "__main__": 71 | print("Run with pytest") 72 | exit(1) 73 | --------------------------------------------------------------------------------