├── .coveragerc ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── NOTICE ├── README.md ├── bin └── README ├── build-tools └── bin │ └── python-builds ├── doc ├── conf.py └── index.rst ├── setup.cfg ├── setup.py ├── src └── ssm_document_generator │ ├── __init__.py │ ├── build_tools │ ├── __init__.py │ ├── ssm_generator.py │ └── ssm_generator_build.py │ ├── command │ ├── __init__.py │ ├── result.py │ ├── result_status.py │ └── result_type.py │ ├── definition │ ├── __init__.py │ ├── bash_definition.py │ ├── bash_formatted_definition.py │ ├── definition.py │ ├── metadata │ │ ├── __init__.py │ │ ├── common.py │ │ ├── document_tags_metadata_mixin.py │ │ ├── ssm_parameter_store_metadata_mixin.py │ │ └── tag.py │ ├── mixins │ │ ├── __init__.py │ │ ├── bash_simple_result_format_mixin.py │ │ ├── compressor_mixin.py │ │ ├── generators.py │ │ ├── python_entry_point_mixin.py │ │ ├── read_from_file_mixin.py │ │ ├── run_as_user_mixin.py │ │ └── stickytape_mixin.py │ ├── parameters │ │ ├── __init__.py │ │ ├── assign_parameters_mixin.py │ │ ├── common.py │ │ ├── dict_parameters_mixin.py │ │ └── parameter.py │ ├── python_definition.py │ └── utils │ │ ├── __init__.py │ │ └── definition_troposphere_adapter.py │ ├── discovery │ ├── __init__.py │ └── discovery.py │ ├── document_manager.py │ ├── examples │ ├── __init__.py │ ├── dmesg │ │ ├── __init__.py │ │ ├── dmesg.sh │ │ └── dmesg_definition.py │ ├── get_file │ │ ├── __init__.py │ │ ├── get_file.py │ │ ├── get_file_definition.py │ │ └── lines_filter.py │ ├── md5sum_definition.py │ └── pstack_definition.py │ ├── main.py │ └── utils │ ├── __init__.py │ └── constants.py ├── test ├── README ├── __init__.py └── ssm_document_generator_test │ ├── __init__.py │ ├── definition │ ├── __init__.py │ ├── dummy_definition.py │ ├── metadata │ │ ├── __init__.py │ │ └── test_tag.py │ ├── mixins │ │ ├── __init__.py │ │ ├── test_compressor_mixin.py │ │ ├── test_generators.py │ │ ├── test_python_entry_point_mixin.py │ │ ├── test_read_from_file_mixin.py │ │ └── test_run_as_user_mixin.py │ ├── parameters │ │ ├── __init__.py │ │ ├── test_assign_parameters_mixin.py │ │ └── test_dict_parameters_mixin.py │ └── test_definition.py │ └── utils │ ├── __init__.py │ ├── test_result.py │ └── test_utils.py └── tools └── test_document.sh /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = true 3 | source = ssm_document_generator 4 | 5 | [paths] 6 | source = 7 | src/ssm_document_generator 8 | build/lib/*/site-packages/ssm_document_generator 9 | 10 | [html] 11 | directory = build/brazil-documentation/coverage 12 | 13 | [xml] 14 | output = build/brazil-documentation/coverage/coverage.xml 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/awslabs/aws-systems-manager-document-generator/issues), or [recently closed](https://github.com/awslabs/aws-systems-manager-document-generator/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/aws-systems-manager-document-generator/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/awslabs/aws-systems-manager-document-generator/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include src *.sh -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2017-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | Licensed under the Apache License, Version 2.0 (the "License"). 3 | You may not use this file except in compliance with the License. A copy of the License is located at http://aws.amazon.com/apache2.0/ or in the "license" file accompanying this file. 4 | This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS Systems Manager Document Generator 2 | 3 | A library + DSL that allows you to easily define SSM documents and convert you your Python or Bash programs into SSM documents (and optionally - CloudFormation templates, containing those documents). 4 | 5 | You can find examples of command definitions in `src/ssm_document_generator/examples`. 6 | 7 | ## Workflow 8 | 9 | **Option 1. Calling the generator directly:** 10 | 11 | 1. Create a definition file. 12 | The file can either directly contain the commands you want to execute or can point to a Python or a Bash file that serves as entry point for your program. 13 | You can find an example of definition file [here](src/ssm_document_generator/examples/get_file/get_file_definition.py) 14 | 1. Run the program and point it at the definition file you've created. I.e. if we are to use it with the definition mentioned above we'd do something like: 15 | `python main.py get_file_definition.py get_file.json` 16 | `get_file.json` here is the output file where the resulting SSM document would be written to. 17 | You can see the description of other available options below. 18 | 19 | **Option 2. Integrate the document creation into your package build process:** 20 | 21 | If you have a Python package with several SSM command definitions present - you can integrate the document creation into your build process by using [build tasks](src/ssm_document_generator/build_tools) provided with this package. 22 | You would do that by adding the following into your `setup.py`: 23 | ```python 24 | #... 25 | setup( 26 | #... 27 | cmdclass={ 28 | 'ssm_generator': SSMGenerator, 29 | 'install_scripts': SSMGeneratorBuild 30 | }, 31 | #... 32 | ) 33 | 34 | ``` 35 | 36 | ## Command usage: 37 | 38 | ``` 39 | usage: main.py [-h] [-cf] [--indent INDENT] input output 40 | 41 | positional arguments: 42 | input A definition file or a directory containing multiple definition files. 43 | output Output file or directory. Should match to input (e.g. 44 | if input is a file - this expected to be a file, 45 | correspondingly if input is directory - this is 46 | expected to be a directory). 47 | 48 | optional arguments: 49 | -h, --help show this help message and exit 50 | -cf, --cloud-formation 51 | Generate CloudFormation template instead of just SSM 52 | document 53 | --indent INDENT Indent for resulting json for SSM document 54 | ``` 55 | 56 | ## License 57 | 58 | This library is licensed under the Apache 2.0 License. 59 | -------------------------------------------------------------------------------- /bin/README: -------------------------------------------------------------------------------- 1 | Put your python scripts into this directory. 2 | 3 | Any script that has a shebang line with python in it and is executable 4 | will be automatically included in your package. All others must be 5 | declared explicitly in the setup.py file. 6 | -------------------------------------------------------------------------------- /build-tools/bin/python-builds: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import re 4 | import sys 5 | 6 | # Build only for python3.6 or greater. 7 | regex = ".*Py.*3([6-9]|[1-9][0-9])" 8 | if not re.match(regex, sys.argv[1]): 9 | exit(1) 10 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | 4 | import pkg_resources 5 | import sys, os 6 | 7 | version = '1.0' 8 | project = u'SSMDocumentGenerator' 9 | 10 | # Add any Sphinx extension module names here, as strings. They can be extensions 11 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 12 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 13 | 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 14 | 'sphinx.ext.coverage', 'sphinx.ext.autosummary', 15 | 'sphinx.ext.napoleon'] 16 | 17 | # Add any paths that contain templates here, relative to this directory. 18 | templates_path = ['_templates'] 19 | 20 | source_suffix = '.rst' # The suffix of source filenames. 21 | master_doc = 'index' # The master toctree document. 22 | 23 | copyright = u'%s, Amazon' % datetime.now().year 24 | 25 | # The full version, including alpha/beta/rc tags. 26 | release = version 27 | 28 | # List of directories, relative to source directory, that shouldn't be searched 29 | # for source files. 30 | exclude_trees = ['_build'] 31 | 32 | pygments_style = 'sphinx' 33 | 34 | autoclass_content = "both" 35 | autodoc_default_flags = ['show-inheritance', 'members', 'undoc-members'] 36 | autodoc_member_order = 'bysource' 37 | 38 | html_theme = 'haiku' 39 | html_static_path = ['_static'] 40 | htmlhelp_basename = '%sdoc' % project 41 | 42 | # Example configuration for intersphinx: refer to the Python standard library. 43 | intersphinx_mapping = {'http://docs.python.org/': None} 44 | 45 | # autosummary 46 | autosummary_generate = True 47 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | SSMDocumentGenerator 2 | ============= 3 | 4 | Please replace this text with a short description of your package. 5 | 6 | 7 | Modules 8 | _______ 9 | 10 | .. autosummary:: 11 | :toctree: generated 12 | 13 | .. Add/replace module names you want documented here 14 | ssm_document_generator 15 | 16 | 17 | Indices and tables 18 | __________________ 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # Test args for pytest; enable coverage for ssm_document_generator, emit XML, 2 | # HTML, and terminal reports. 3 | [tool:pytest] 4 | addopts = 5 | --verbose 6 | --ignore=build/private 7 | --cov ssm_document_generator 8 | --cov-report term-missing 9 | --cov-report xml 10 | --cov-report html 11 | test 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | from glob import glob 3 | from setuptools import setup, find_packages 4 | 5 | data_files = [] 6 | 7 | # declare your scripts: 8 | # scripts in bin/ with a shebang containing python will be 9 | # recognized automatically 10 | scripts = [] 11 | for fname in glob('bin/*'): 12 | with open(fname, 'r') as fh: 13 | if re.search(r'^#!.*python', fh.readline()): 14 | scripts.append(fname) 15 | 16 | setup(name="SSMDocumentGenerator", 17 | version="1.0", 18 | 19 | # declare your packages 20 | packages=find_packages(where="src", exclude=("test",)), 21 | package_dir={"": "src"}, 22 | 23 | # declare your scripts 24 | scripts=scripts, 25 | 26 | # include data files 27 | data_files=data_files, 28 | 29 | # set up the shebang 30 | options={ 31 | # make sure the right shebang is set for the scripts - use the environment default Python 32 | 'build_scripts': { 33 | 'executable': '/apollo/sbin/envroot "$ENVROOT/bin/python"', 34 | }, 35 | }, 36 | 37 | # Ship the python 3.6 script in $ROOT/bin. Remove this flag to skip 38 | # installing scripts into $ROOT/bin/ 39 | # 40 | # Note that if your versionset doesn't build for 3.6, this won't do the right thing! 41 | root_script_source_version="python3.6", 42 | 43 | # When we have something that's only for one version, use 3.6 44 | default_python="python3.6", 45 | 46 | test_command='brazilpython_pytest', 47 | 48 | # Use Sphinx for docs 49 | doc_command='build_sphinx', 50 | include_package_data=True) 51 | -------------------------------------------------------------------------------- /src/ssm_document_generator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-systems-manager-document-generator/2c041fd52342d95da4535fe3236e43933cc6e08d/src/ssm_document_generator/__init__.py -------------------------------------------------------------------------------- /src/ssm_document_generator/build_tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-systems-manager-document-generator/2c041fd52342d95da4535fe3236e43933cc6e08d/src/ssm_document_generator/build_tools/__init__.py -------------------------------------------------------------------------------- /src/ssm_document_generator/build_tools/ssm_generator.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from setuptools import Command 3 | 4 | from ssm_document_generator.document_manager import DocumentManager 5 | 6 | 7 | class SSMGenerator(Command): 8 | """ 9 | A custom command to generate CloudFormation template with your SSM documents from your command definitions. 10 | """ 11 | 12 | description = 'generate SSM documents from your command definitions' 13 | user_options = [ 14 | ('source-directory=', None, 'path to the directory to scan for command definitions'), 15 | ('destination-path=', None, 'where to put resulting template'), 16 | ] 17 | 18 | def initialize_options(self): 19 | self.source_directory = 'src' 20 | self.destination_path = 'build/cloudFormation/ssm_commands.template' 21 | 22 | def finalize_options(self): 23 | source_path = Path(self.source_directory) 24 | assert source_path.exists() and source_path.is_dir(), \ 25 | "Specified source directory {} does not exist".format(self.source_directory) 26 | 27 | def run(self): 28 | template = DocumentManager.cloudformation_template(DocumentManager.load_from_directory(self.source_directory)) 29 | Path(self.destination_path).write_text(template.to_json()) 30 | -------------------------------------------------------------------------------- /src/ssm_document_generator/build_tools/ssm_generator_build.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from pathlib import Path 4 | from setuptools.command.install_scripts import install_scripts 5 | 6 | 7 | class SSMGeneratorBuild(install_scripts): 8 | """ 9 | Defines override of build_py step that has ssm_generator step as prerequisite. 10 | Use this to build CloudFormation template from your generated SSM documents. 11 | """ 12 | 13 | def run(self): 14 | super().run() 15 | Path('build/cloudFormation').mkdir(parents=True, exist_ok=True) 16 | 17 | # This is needed if we want to use the modules defined in the same package as definition, as the 18 | # directory for the module does not exist initially - the path_importer stored in cache is None 19 | # And so we need to clean cache to recreate the importers. 20 | sys.path_importer_cache.clear() 21 | self.run_command('ssm_generator') 22 | -------------------------------------------------------------------------------- /src/ssm_document_generator/command/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-systems-manager-document-generator/2c041fd52342d95da4535fe3236e43933cc6e08d/src/ssm_document_generator/command/__init__.py -------------------------------------------------------------------------------- /src/ssm_document_generator/command/result.py: -------------------------------------------------------------------------------- 1 | import json 2 | import traceback 3 | 4 | from ssm_document_generator.command.result_status import ResultStatus 5 | from ssm_document_generator.command.result_type import ResultType 6 | 7 | 8 | class Result: 9 | """ 10 | Util for simplifying providing unified interface to generated commands 11 | """ 12 | 13 | @classmethod 14 | def success(cls, result_data, metadata=None): 15 | if metadata is None: 16 | metadata = {} 17 | 18 | return {**{'status': ResultStatus.Success.value, 'result': result_data}, **metadata} 19 | 20 | @classmethod 21 | def failure(cls, error, message=None, metadata=None): 22 | if metadata is None: 23 | metadata = {} 24 | 25 | return {**{'status': ResultStatus.Failure.value, 'status_details': type(error).__name__, 26 | 'message': message if message else str(error)}, 27 | **metadata} 28 | 29 | @classmethod 30 | def run(cls, runnable, result_type=ResultType.JSON): 31 | """ 32 | Try running passed runnable, if run is successful return Result.success if run threw some exception 33 | catch it and return Result.failure. 34 | If functions has parameters - you should pass a lambda with the function call. It's done like that 35 | so that if any exceptions arise at the parameter evaluation time - they would be caught. 36 | """ 37 | metadata = {'result_type': result_type.value} 38 | 39 | try: 40 | result = Result.success(runnable(), metadata) 41 | except Exception as error: 42 | result = Result.failure(error, message=traceback.format_exc(), metadata=metadata) 43 | finally: 44 | print(json.dumps(result, sort_keys=True, default=str)) 45 | return result 46 | -------------------------------------------------------------------------------- /src/ssm_document_generator/command/result_status.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ResultStatus(Enum): 5 | Success = 'Success' 6 | Failure = 'Failed' 7 | -------------------------------------------------------------------------------- /src/ssm_document_generator/command/result_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ResultType(Enum): 5 | JSON = 'JSON' 6 | TableData = 'TableData' 7 | PlainText = 'PlainText' 8 | -------------------------------------------------------------------------------- /src/ssm_document_generator/definition/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-systems-manager-document-generator/2c041fd52342d95da4535fe3236e43933cc6e08d/src/ssm_document_generator/definition/__init__.py -------------------------------------------------------------------------------- /src/ssm_document_generator/definition/bash_definition.py: -------------------------------------------------------------------------------- 1 | from ssm_document_generator.definition.metadata.document_tags_metadata_mixin import DocumentTagsMetadataMixin 2 | from ssm_document_generator.definition.mixins.run_as_user_mixin import RunAsUserMixin 3 | from ssm_document_generator.definition.mixins.read_from_file_mixin import ReadFromFileMixin 4 | 5 | from ssm_document_generator.definition.parameters.assign_parameters_mixin import AssignParametersMixin 6 | from ssm_document_generator.definition.definition import Definition 7 | 8 | 9 | class BashDefinition(ReadFromFileMixin, 10 | RunAsUserMixin, 11 | AssignParametersMixin, 12 | DocumentTagsMetadataMixin, 13 | Definition): 14 | """ 15 | Default definition for bash commands 16 | """ 17 | -------------------------------------------------------------------------------- /src/ssm_document_generator/definition/bash_formatted_definition.py: -------------------------------------------------------------------------------- 1 | from ssm_document_generator.definition.bash_definition import BashDefinition 2 | from ssm_document_generator.definition.mixins.bash_simple_result_format_mixin import BashSimpleResultFormatMixin 3 | 4 | 5 | class BashFormattedDefinition(BashSimpleResultFormatMixin, BashDefinition): 6 | """ 7 | A definition for bash commands with formatted output template 8 | """ 9 | -------------------------------------------------------------------------------- /src/ssm_document_generator/definition/definition.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | from copy import deepcopy 4 | 5 | from ssm_document_generator.definition.parameters.parameter import Parameter 6 | from ssm_document_generator.utils import constants 7 | 8 | 9 | class Definition: 10 | """ 11 | The base Definition class that serves as a foundation for any document definition. You can introduce additional 12 | functionality by inheriting from this class and using one of the provided mixins, one of your own mixins 13 | or by just overriding the functions of this class. 14 | """ 15 | 16 | DEFAULT_PARAMETERS = [ 17 | Parameter('executionTimeout', 18 | '(Optional) The time in seconds for a command to complete ' 19 | 'before it is considered to have failed. Default is 120.' 20 | ' Maximum is 28800 (8 hours).', 21 | default='120', 22 | allowed_pattern='([1-9][0-9]{0,3})|(1[0-9]{1,4})|(2[0-7][0-9]{1,3})|(28[0-7][0-9]{1,2})|(28800)') 23 | ] 24 | 25 | DOCUMENT_TEMPLATE = { 26 | "mainSteps": [ 27 | { 28 | "action": "aws:runShellScript", 29 | "name": "runShellScript", 30 | "inputs": { 31 | "id": "0.runShellScript", 32 | "runCommand": [], 33 | "timeoutSeconds": "{{ executionTimeout }}" 34 | } 35 | } 36 | ] 37 | } 38 | 39 | FIELDS_TO_COPY = ['description', 'schemaVersion'] 40 | 41 | def __init__(self, name, 42 | description, 43 | parameters=None, 44 | interpreter='bash', 45 | schema_version='2.2', 46 | commands=None): 47 | self.name = name 48 | self.description = description 49 | self.parameters = self.DEFAULT_PARAMETERS + (parameters or []) 50 | self.interpreter = interpreter 51 | self.schemaVersion = schema_version 52 | self.commands = commands or [] 53 | 54 | def ssm_document(self): 55 | """ 56 | Return the ssm document in the form of the dictionary, based on the definition. 57 | """ 58 | document = deepcopy(self.DOCUMENT_TEMPLATE) 59 | self.copy_fields(document) 60 | self.add_parameters(document) 61 | self.add_code(document) 62 | return document 63 | 64 | def get_metadata(self): 65 | """ 66 | Returns tags list, where value depends on implementation. 67 | :return: 68 | """ 69 | return {} 70 | 71 | def get_complimentary_cfn_resources(self): 72 | """ 73 | Entry point to introduce additional CloudFormation resources to accompany the document. 74 | :return: 75 | """ 76 | return [] 77 | 78 | def copy_fields(self, document): 79 | """ 80 | Copy the defined set of fields from the definition to the provided document dictionary. 81 | """ 82 | for field in self.FIELDS_TO_COPY: 83 | document[field] = getattr(self, field) 84 | 85 | def add_parameters(self, document): 86 | """ 87 | Add the parameters description to the given document dictionary. 88 | """ 89 | document.setdefault('parameters', {}) 90 | for parameter in self.parameters: 91 | parameter.add_to_dict(document['parameters']) 92 | 93 | def add_code(self, document): 94 | """ 95 | Add the commands to be executed to the given document dictionary. 96 | """ 97 | document['mainSteps'][0]['inputs']['runCommand'] = \ 98 | list(itertools.chain(self.prefix_code(), 99 | self.generate_parameters_code(), 100 | self.generate_commands(), 101 | self.postfix_code())) 102 | 103 | def shebang(self): 104 | return constants.SHEBANG_ENV + ' ' + self.interpreter 105 | 106 | def generate_commands(self): 107 | return self.commands 108 | 109 | def generate_parameters_code(self): 110 | """ 111 | From given parameter definition - generate code to pass the parameters to the command implementation 112 | """ 113 | return [] 114 | 115 | def prefix_code(self): 116 | """ 117 | Returns code that should be added at the beginning of the generated script before the main body of code. 118 | """ 119 | return [self.shebang()] 120 | 121 | def postfix_code(self): 122 | """ 123 | Returns code that should be added at the end of the generated script after the main body of code. 124 | """ 125 | return [] 126 | -------------------------------------------------------------------------------- /src/ssm_document_generator/definition/metadata/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-systems-manager-document-generator/2c041fd52342d95da4535fe3236e43933cc6e08d/src/ssm_document_generator/definition/metadata/__init__.py -------------------------------------------------------------------------------- /src/ssm_document_generator/definition/metadata/common.py: -------------------------------------------------------------------------------- 1 | from ssm_document_generator.definition.metadata.tag import Tag 2 | 3 | 4 | def category(category_name): 5 | return Tag('category', category_name) 6 | 7 | 8 | def access_level(level): 9 | return Tag('accessLevel', level) 10 | -------------------------------------------------------------------------------- /src/ssm_document_generator/definition/metadata/document_tags_metadata_mixin.py: -------------------------------------------------------------------------------- 1 | class DocumentTagsMetadataMixin: 2 | """ 3 | Adds metadata support in form of tags on SSM document. 4 | """ 5 | 6 | def __init__(self, metadata=None, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | self.tags = metadata or [] 9 | 10 | def get_metadata(self): 11 | return {**{tag.name: tag.value for tag in self.tags}, **super().get_metadata()} 12 | -------------------------------------------------------------------------------- /src/ssm_document_generator/definition/metadata/ssm_parameter_store_metadata_mixin.py: -------------------------------------------------------------------------------- 1 | class SSMParameterStoreMetadataMixin: 2 | """ 3 | Adds metadata support in form of things stored to SSM parameter store to definition. 4 | """ 5 | 6 | def __init__(self, metadata=None, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | self.tags = metadata or [] 9 | 10 | def get_complimentary_cfn_resources(self): 11 | return [tag.as_ssm_parameter(self.name) for tag in self.tags] + super().get_complimentary_cfn_resources() 12 | -------------------------------------------------------------------------------- /src/ssm_document_generator/definition/metadata/tag.py: -------------------------------------------------------------------------------- 1 | from troposphere import ssm, Sub 2 | 3 | 4 | class Tag: 5 | """ 6 | The tag for SSM document command. 7 | """ 8 | 9 | def __init__(self, name, value): 10 | self.name = name 11 | self.value = value 12 | 13 | def as_ssm_parameter(self, command_name): 14 | """ 15 | Return representation of key-value tag for command in a form of SSM ParameterStore parameter. 16 | :param command_name: 17 | :return: 18 | """ 19 | return ssm.Parameter("{}{}Tag".format(command_name, self.name.title()), 20 | Name=Sub('/owls/${{AWS::StackName}}/{}/{}'.format(command_name, self.name)), 21 | Type=self.get_type(self.value), 22 | Value=str(self.value)) 23 | 24 | @staticmethod 25 | def get_type(value): 26 | if type(value) is list: 27 | return 'StringList' 28 | 29 | return 'String' 30 | -------------------------------------------------------------------------------- /src/ssm_document_generator/definition/mixins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-systems-manager-document-generator/2c041fd52342d95da4535fe3236e43933cc6e08d/src/ssm_document_generator/definition/mixins/__init__.py -------------------------------------------------------------------------------- /src/ssm_document_generator/definition/mixins/bash_simple_result_format_mixin.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | from ssm_document_generator.definition.parameters.common import entities_limit, regex_filter_expression 4 | 5 | bash_code_template = """ 6 | resultStatus="Success" 7 | 8 | process_result() { 9 | result=$(echo "${result}" | grep -E "${filterExpression}") 10 | 11 | if ((${entitiesLimit} != 0)); then 12 | result=$(echo "${result}" | tail -${entitiesLimit}) 13 | fi 14 | } 15 | 16 | command () { 17 | set -e 18 | %s 19 | } 20 | 21 | result=$(command) 22 | 23 | if [ $? -eq 0 ]; then 24 | process_result 25 | else 26 | resultStatus="Failed" 27 | fi 28 | 29 | result=$(echo "${result}" | jq -R -s -c '.') 30 | 31 | cat <; 7 | to add other packages, modify pytest.ini in the package root directory. 8 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-systems-manager-document-generator/2c041fd52342d95da4535fe3236e43933cc6e08d/test/__init__.py -------------------------------------------------------------------------------- /test/ssm_document_generator_test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-systems-manager-document-generator/2c041fd52342d95da4535fe3236e43933cc6e08d/test/ssm_document_generator_test/__init__.py -------------------------------------------------------------------------------- /test/ssm_document_generator_test/definition/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-systems-manager-document-generator/2c041fd52342d95da4535fe3236e43933cc6e08d/test/ssm_document_generator_test/definition/__init__.py -------------------------------------------------------------------------------- /test/ssm_document_generator_test/definition/dummy_definition.py: -------------------------------------------------------------------------------- 1 | from ssm_document_generator.definition.definition import Definition 2 | 3 | 4 | class DummyDefinition(Definition): 5 | def __init__(self, *args, **kwargs): 6 | super().__init__(name='testName', description='testDescription', *args, **kwargs) 7 | -------------------------------------------------------------------------------- /test/ssm_document_generator_test/definition/metadata/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-systems-manager-document-generator/2c041fd52342d95da4535fe3236e43933cc6e08d/test/ssm_document_generator_test/definition/metadata/__init__.py -------------------------------------------------------------------------------- /test/ssm_document_generator_test/definition/metadata/test_tag.py: -------------------------------------------------------------------------------- 1 | from troposphere import Sub 2 | 3 | from ssm_document_generator.definition.metadata.tag import Tag 4 | 5 | 6 | def test_type_str(): 7 | assert 'String' == Tag.get_type('a') 8 | 9 | 10 | def test_type_list(): 11 | assert 'StringList' == Tag.get_type(['a']) 12 | 13 | 14 | def test_as_ssm_parameter(): 15 | ttag = Tag('tname', 'tvalue') 16 | t_parameter = ttag.as_ssm_parameter('tcommand') 17 | 18 | assert t_parameter.title == 'tcommandTnameTag' 19 | assert t_parameter.Name.to_dict() == Sub('/owls/${AWS::StackName}/tcommand/tname').to_dict() 20 | assert t_parameter.Type == 'String' 21 | assert t_parameter.Value == 'tvalue' 22 | -------------------------------------------------------------------------------- /test/ssm_document_generator_test/definition/mixins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-systems-manager-document-generator/2c041fd52342d95da4535fe3236e43933cc6e08d/test/ssm_document_generator_test/definition/mixins/__init__.py -------------------------------------------------------------------------------- /test/ssm_document_generator_test/definition/mixins/test_compressor_mixin.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | from ssm_document_generator.definition.mixins.compressor_mixin import CompressorMixin 4 | from test.ssm_document_generator_test.definition.dummy_definition import DummyDefinition 5 | 6 | 7 | class Mixed(CompressorMixin, DummyDefinition): 8 | pass 9 | 10 | 11 | def test_compression(): 12 | test_object = lambda: None 13 | test_code = ''' 14 | test_object.test_var = 'success' 15 | ''' 16 | assert getattr(test_object, 'test_var', None) is None 17 | 18 | tdefinition = Mixed(commands=textwrap.dedent(test_code).splitlines()) 19 | 20 | exec('\n'.join(tdefinition.ssm_document()['mainSteps'][0]['inputs']['runCommand'])) 21 | 22 | assert getattr(test_object, 'test_var', None) == 'success' 23 | -------------------------------------------------------------------------------- /test/ssm_document_generator_test/definition/mixins/test_generators.py: -------------------------------------------------------------------------------- 1 | from ssm_document_generator.definition.mixins.generators import add_params_mixin 2 | 3 | 4 | def test_add_params_mixin(): 5 | class TestBase: 6 | parameters = [] 7 | 8 | class TestMix(add_params_mixin(['test']), TestBase): 9 | pass 10 | 11 | assert TestMix().parameters == ['test'] 12 | 13 | 14 | def test_add_params_mixin_init(): 15 | class TestBase: 16 | def __init__(self, tparam): 17 | self.tparam = tparam 18 | self.parameters = [] 19 | 20 | class TestMix(add_params_mixin(tparam='tparam'), TestBase): 21 | pass 22 | 23 | assert TestMix().tparam == 'tparam' 24 | assert TestMix().parameters == [] 25 | -------------------------------------------------------------------------------- /test/ssm_document_generator_test/definition/mixins/test_python_entry_point_mixin.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from ssm_document_generator.definition.mixins.python_entry_point_mixin import PythonEntryPointMixin 4 | from ssm_document_generator.utils import constants 5 | from test.ssm_document_generator_test.definition.dummy_definition import DummyDefinition 6 | 7 | 8 | class Mixed(PythonEntryPointMixin, DummyDefinition): 9 | pass 10 | 11 | 12 | def test_python_entry_point(): 13 | tobject = Mixed() 14 | tdoc = deepcopy(tobject.DOCUMENT_TEMPLATE) 15 | tobject.parameters = [] 16 | tobject.add_code(tdoc) 17 | 18 | assert tdoc['mainSteps'][0]['inputs']['runCommand'] == \ 19 | [constants.SHEBANG_ENV + ' python3', tobject.postfix_code()[0]] 20 | -------------------------------------------------------------------------------- /test/ssm_document_generator_test/definition/mixins/test_read_from_file_mixin.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from tempfile import NamedTemporaryFile 3 | 4 | import pytest 5 | 6 | from ssm_document_generator.definition.mixins.read_from_file_mixin import ReadFromFileMixin 7 | from test.ssm_document_generator_test.definition.dummy_definition import DummyDefinition 8 | 9 | 10 | class Mixed(ReadFromFileMixin, DummyDefinition): 11 | pass 12 | 13 | 14 | def test_read_from_file(): 15 | temp_commands = ['wh', 'eee'] 16 | with NamedTemporaryFile(mode='w') as temp_file: 17 | temp_file.write('\n'.join(temp_commands)) 18 | temp_file.flush() 19 | 20 | tobject = Mixed(command_file_name=temp_file.name) 21 | assert tobject.generate_commands() == temp_commands 22 | 23 | 24 | def test_infer_command_directory(): 25 | assert ReadFromFileMixin.infer_command_directory('test_read_from_file_mixin.py') == Path(__file__).parent 26 | 27 | 28 | def test_infer_command_directory_not_found(): 29 | with pytest.raises(RuntimeError): 30 | ReadFromFileMixin.infer_command_directory('6246c60c-87d7-4b6c-ad89-78a2bdc63962') # random uuid 31 | -------------------------------------------------------------------------------- /test/ssm_document_generator_test/definition/mixins/test_run_as_user_mixin.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from ssm_document_generator.definition.mixins.run_as_user_mixin import RunAsUserMixin 4 | from ssm_document_generator.utils import constants 5 | from test.ssm_document_generator_test.definition.dummy_definition import DummyDefinition 6 | 7 | 8 | class Mixed(RunAsUserMixin, DummyDefinition): 9 | pass 10 | 11 | 12 | def test_empty_run_as_user(): 13 | user = 'tuser' 14 | tobject = Mixed(user=user) 15 | tdoc = deepcopy(tobject.DOCUMENT_TEMPLATE) 16 | tobject.parameters = [] 17 | tobject.add_code(tdoc) 18 | 19 | assert tdoc['mainSteps'][0]['inputs']['runCommand'] == \ 20 | [constants.SHEBANG_ENV + ' bash', 21 | "su - {} -c '{} -' <<'{}'".format(user, 'bash', str(tobject.uuid)), 22 | str(tobject.uuid)] 23 | -------------------------------------------------------------------------------- /test/ssm_document_generator_test/definition/parameters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-systems-manager-document-generator/2c041fd52342d95da4535fe3236e43933cc6e08d/test/ssm_document_generator_test/definition/parameters/__init__.py -------------------------------------------------------------------------------- /test/ssm_document_generator_test/definition/parameters/test_assign_parameters_mixin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ssm_document_generator.definition.parameters.common import entities_limit 4 | from ssm_document_generator.definition.parameters.dict_parameters_mixin import DictParametersMixin 5 | from test.ssm_document_generator_test.definition.dummy_definition import DummyDefinition 6 | 7 | 8 | class Mixed(DictParametersMixin, DummyDefinition): 9 | pass 10 | 11 | 12 | @pytest.mark.parametrize('test_input, expected', [ 13 | ([], ['parameters = {}']), 14 | ([entities_limit()], ["parameters = {\"" + entities_limit().name + "\": \"{{" + entities_limit().name + "}}\"}"]) 15 | ]) 16 | def test_generate_parameters_code(test_input, expected): 17 | tobject = Mixed() 18 | tobject.parameters = test_input 19 | assert tobject.generate_parameters_code() == expected 20 | -------------------------------------------------------------------------------- /test/ssm_document_generator_test/definition/parameters/test_dict_parameters_mixin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ssm_document_generator.definition.parameters.assign_parameters_mixin import AssignParametersMixin 4 | from ssm_document_generator.definition.parameters.common import entities_limit 5 | from test.ssm_document_generator_test.definition.dummy_definition import DummyDefinition 6 | 7 | 8 | class Mixed(AssignParametersMixin, DummyDefinition): 9 | pass 10 | 11 | 12 | @pytest.mark.parametrize('test_input, expected', [ 13 | ([], []), 14 | ([entities_limit()], [entities_limit().name + '={{' + entities_limit().name + '}}']) 15 | ]) 16 | def test_generate_parameters_code(test_input, expected): 17 | tobject = Mixed() 18 | tobject.parameters = test_input 19 | assert tobject.generate_parameters_code() == expected 20 | -------------------------------------------------------------------------------- /test/ssm_document_generator_test/definition/test_definition.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ssm_document_generator.definition.definition import Definition 4 | 5 | 6 | def test_copy_fields(): 7 | tdef = Definition('tdef', 'tdef') 8 | tresult = {} 9 | tdef.copy_fields(tresult) 10 | assert tresult == {'description': 'tdef', 'schemaVersion': '2.2'} 11 | 12 | 13 | def test_add_parameters(): 14 | tdef = Definition('tdef', 'tdef') 15 | tresult = {} 16 | tdef.add_parameters(tresult) 17 | expected = {'parameters': {parameter.name: parameter.get_dict() for parameter in tdef.DEFAULT_PARAMETERS}} 18 | assert tresult == expected 19 | -------------------------------------------------------------------------------- /test/ssm_document_generator_test/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-systems-manager-document-generator/2c041fd52342d95da4535fe3236e43933cc6e08d/test/ssm_document_generator_test/utils/__init__.py -------------------------------------------------------------------------------- /test/ssm_document_generator_test/utils/test_result.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ssm_document_generator.command.result_status import ResultStatus 4 | from ssm_document_generator.command.result import Result 5 | 6 | 7 | @pytest.mark.parametrize('test_input', [ 8 | [], 9 | [1, 2], 10 | {'foo': 'bar'} 11 | ]) 12 | def test_success(test_input): 13 | assert Result.success(test_input) == {'status': ResultStatus.Success.value, 'result': test_input} 14 | 15 | 16 | @pytest.mark.parametrize('error, message, expected', [ 17 | (RuntimeError('tm1'), None, 18 | {'status': ResultStatus.Failure.value, 'status_details': 'RuntimeError', 'message': 'tm1'}), 19 | (RuntimeError('tm1'), 'tm2', 20 | {'status': ResultStatus.Failure.value, 'status_details': 'RuntimeError', 'message': 'tm2'}), 21 | ]) 22 | def test_failure(error, message, expected): 23 | assert Result.failure(error, message) == expected 24 | 25 | 26 | def raiser(exception): 27 | """ 28 | Need this to work around limitation of the fact that I can't have just a statement in lambda 29 | """ 30 | raise exception 31 | 32 | 33 | @pytest.mark.parametrize('runnable, expected', [ 34 | (lambda: [], Result.success([], metadata={'result_type': 'JSON'})), 35 | (lambda: raiser(RuntimeError('t1')), Result.failure(RuntimeError('t1'), metadata={'result_type': 'JSON'})) 36 | ]) 37 | def test_run(runnable, expected): 38 | result = Result.run(runnable) 39 | result.pop('message', None) 40 | expected.pop('message', None) 41 | # Don't compare messages, as in run its traceback. 42 | assert result == expected 43 | -------------------------------------------------------------------------------- /test/ssm_document_generator_test/utils/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ssm_document_generator import utils 4 | 5 | 6 | @pytest.mark.parametrize('test_input, expected', [ 7 | ({}, {}), 8 | ({'test': 'test'}, {'test': 'test'}), 9 | ({'test': 'test', 'tnone': None}, {'test': 'test'}), 10 | ]) 11 | def test_dict_without_none_entries(test_input, expected): 12 | assert utils.dict_without_none_entries(test_input) == expected 13 | -------------------------------------------------------------------------------- /tools/test_document.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Helper bash script to upload and test generated document. 3 | #stickytape ./test_module/test_file_complex.py --output-file stickytype_test.py 4 | 5 | #pyminifier --lzma -o minified.py stickytype_test.py 6 | #pyminifier --bzip2 -o minified.py stickytype_test.py 7 | 8 | 9 | documentToUpload=../src/ssm_to_upload.json 10 | # aws ssm create-document --content file://${documentToUpload} --name "generate_test" --document-type "Command" --region us-east-1 11 | documentVersion=$(aws ssm update-document --content file://${documentToUpload} --name "generate_test" --document-version "\$LATEST" --output text --query "DocumentDescription.DocumentVersion" --region us-east-1) 12 | 13 | aws ssm update-document-default-version --name "generate_test" --document-version "${documentVersion}" --region us-east-1 14 | 15 | instanceId=i-0cc6d175929051fd1 16 | #invocationId=$(aws ssm send-command --instance-id ${instanceId} --document-name "generate_test" --parameters '{"excludeFilter":["blah", "foo"], "filePath": ["/var/log/messages"]}' --output text --query "Command.CommandId" --region us-east-1 --output-s3-bucket-name sitalov-ssm-test2 --output-s3-key-prefix OWLS) 17 | #invocationId=$(aws ssm send-command --instance-id ${instanceId} --document-name "generate_test" --parameters '{"filePath": ["/var/log/messages"]}' --output text --query "Command.CommandId" --region us-east-1 --output-s3-bucket-name sitalov-ssm-test2 --output-s3-key-prefix OWLS) 18 | invocationId=$(aws ssm send-command --instance-id ${instanceId} --document-name "generate_test" --output text --query "Command.CommandId" --region us-east-1 --output-s3-bucket-name sitalov-ssm-test2 --output-s3-key-prefix OWLS) 19 | 20 | sleep 2 21 | 22 | aws ssm get-command-invocation --command-id ${invocationId} --instance-id ${instanceId} --region us-east-1 > command_output 23 | cat command_output 24 | 25 | --------------------------------------------------------------------------------