├── .build_pkg_notes ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── _codebuild └── Buildspec ├── _images └── deploy_to_aws.png ├── _tests └── test.sh ├── aws-cfn-control ├── MANIFEST.in ├── README.md ├── awscfnctl │ ├── __init__.py │ ├── add_ena_vfi.sh │ ├── asgctl.py │ ├── awscfnctl.py │ ├── build_ami_maps.py │ ├── cfnctl.py │ ├── get_asg_from_stack.py │ ├── get_inst_from_asg.py │ ├── get_priv_dns_asg.py │ ├── getamiinfo.py │ ├── getec2keys.py │ ├── getinstinfo.py │ ├── getnetinfo.py │ └── getstackinfo.py ├── setup.cfg └── setup.py ├── copyright.template.json ├── requirements.txt └── troposhpere ├── Instance.py ├── VPC_2x_AZ.py ├── _include └── __init__.py └── instance.template /.build_pkg_notes: -------------------------------------------------------------------------------- 1 | Build python distribution and upload to PyPi 2 | 3 | Some instructions are here, but the steps below take precedence: https://packaging.python.org/tutorials/packaging-projects/ 4 | 5 | Install dependencies: 6 | $ pip install --upgrade setuptools wheel 7 | $ pip install --upgrade twine 8 | 9 | Change directories to package directory 10 | 11 | Update setup.py with new version 12 | 13 | Build: 14 | $ python setup.py sdist 15 | 16 | Upload with twine, this should prompt for User/Password: 17 | $ twine upload dist/* 18 | 19 | -------------------------------------------------------------------------------- /.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 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # IDE and Editor artifacts # 104 | *.bbprojectd 105 | .idea 106 | *.iml 107 | 108 | # Temporary Files # 109 | tmp_* 110 | cfg.tmp.json 111 | 112 | # OS generated files # 113 | .DS_Store 114 | .DS_Store? 115 | 116 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: python 4 | 5 | python: 6 | - "3.9" 7 | 8 | install: 9 | - cd aws-cfn-control 10 | - pip install -e . 11 | - cd - 12 | 13 | script: 14 | - sh _tests/test.sh 15 | 16 | 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-cfn-control/29e5d602a82b66ddeca3b6761587804a23c6ae3d/CHANGELOG.md -------------------------------------------------------------------------------- /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-CFN-Control/issues), or [recently closed](https://github.com/awslabs/AWS-CFN-Control/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-CFN-Control/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-CFN-Control/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 | 204 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWS CFN Control 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS CFN Control [![Build Status](https://app.travis-ci.com/awslabs/aws-cfn-control.svg?branch=master)](https://travis-ci.org/awslabs/aws-cfn-control) [![PyPi Status](https://badge.fury.io/py/aws-cfn-control.png)](https://badge.fury.io/py/aws-cfn-control) 2 | 3 | AWS-CFN-Control provides a command line interface to quickly deploy and redeploy [AWS CloudFormation stacks](https://aws.amazon.com/cloudformation/). The `cfnctl` command provides the core functionality, with several other commands that will find AMI info, get stack status, build CloudFormation mappings, and other features. AWS CFN Control will handle CloudFormation templates that have parameters, and you want to create stacks with the same parameters in multiple regions, or you want to change just a few parameters values for a new stack. 4 | 5 | 6 | ## License 7 | 8 | This library is licensed under the Apache 2.0 License. 9 | 10 | ## Prerequisites 11 | 12 | It is assumed that you have an AWS account (preferably with admin privileges) and experience with CloudFormation. You will also need to be familiar with either [AWS Cloud Development Kit](https://aws.amazon.com/cdk/) (CDK) or writing your own CloudFormation templates. Although JSON or YAML formatted templates can be used, JSON is recommended. 13 | 14 | ## Installation 15 | 16 | ``` 17 | pip install aws-cfn-control 18 | ``` 19 | 20 | ## TL;DR 21 | 22 | 1. Build cfnctl parameters file 23 | 1. Launch the stack 24 | 1. Check stack status and outputs 25 | 26 | ## Usage overview 27 | 28 | #### Build the parameters file for a CloudFormation template: 29 | This command builds a default parameters file: 30 | 31 | ```cfnctl build -t ``` 32 | 33 | #### Create a stack using the template or a parameters file: 34 | 35 | This first create command (using -t) first checks if there is an existing parameters file, and prompts if it should be used. Otherwise, a parameters file is created using the stack name (-n) appended to the template file name. You are then prompted for the stack parameters (similar to build action), and then the stack is created. 36 | 37 | ```cfnctl create -n stack001 -t ``` 38 | 39 | This second command (using -f) uses an existing parameters file, which has the template location, to create a stack. You will not be prompted for any parameters: 40 | 41 | ```cfnctl create -n stack001 -f ``` 42 | 43 | #### List all existing stacks 44 | 45 | ```cfnctl list``` 46 | 47 | #### List detials of one existing stack 48 | 49 | ```cfnctl list -n stack001``` 50 | 51 | #### Delete a stack 52 | 53 | ```cfnctl delete -n ``` 54 | 55 | ## More detailed information 56 | 57 | ### Command help 58 | 59 | ```text 60 | usage: cfnctl [-h] [-r REGION] [-n STACK_NAME] [-t TEMPLATE] [-f PARAM_FILE] [-d] [-b BUCKET] [-nr] [-p AWS_PROFILE] [-y] [-v] cfn_action 61 | 62 | Launch and manage CloudFormation templates from the command line 63 | 64 | positional arguments: 65 | cfn_action REQUIRED: Action: build|create|list|delete 66 | build Builds the CFN parameter file (-t required) 67 | create Creates a new stack (-n and [-t|-f] required) 68 | list List all stacks (-d provides extra detail) 69 | delete Deletes a stack (-n is required) 70 | 71 | arguments: 72 | -h, --help show this help message and exit 73 | -r REGION Region name 74 | -n STACK_NAME Stack name 75 | -t TEMPLATE CFN Template from local file or S3 URL 76 | -f PARAM_FILE Template parameter file 77 | -d List details on all stacks 78 | -b BUCKET Bucket to upload template to 79 | -nr Do not rollback 80 | -p AWS_PROFILE AWS Profile 81 | -y On interactive question, force yes 82 | -v Verbose config file 83 | ``` 84 | 85 | ### Using the defaults from CloudFormation templates and seeing existing resources 86 | 87 | When using the ```build``` or ```create``` actions, as you are prompted for each parameter you will be given the choice of choosing the default value specified in the template. For example, if your template has this: 88 | ```text 89 | "MyInstanceType": { 90 | "Default": "m5.24xlarge", 91 | "Description": "Instance type", 92 | "Type": "String" 93 | }, 94 | ``` 95 | 96 | Your prompt will have the default value included, and you just hit entire to accept the default: 97 | ```text 98 | MyInstanceType [m5.24xlarge]: 99 | ``` 100 | 101 | #### Existing resources 102 | 103 | For some resources, a list of existing values will be shown. For example, here is what the list of subnets look like, with the default set to ```subnet-abbbcccbbbbcd1235```: 104 | 105 | ```text 106 | Getting subnets for vpc-0000aaaa1111bbbb2 ... 107 | subnet-aaaabbbcccbcd1234 | us-east-1b 108 | subnet-abbbcccbbbbcd12gg | us-east-1-bos-1a | Local Zone Subnet 109 | subnet-abbbcccbbbbcd1235 | us-east-1a 110 | subnet-addddbbbccbcd1232 | us-east-1f 111 | subnet-addddbbbccbcd1236 | us-east-1c 112 | subnet-addddbbbccbcd1238 | us-east-1e 113 | subnet-aaabbbbcccbcd1237 | us-east-1d 114 | Select subnet: [subnet-abbbcccbbbbcd1235]: 115 | ``` 116 | 117 | #### Using a region defaults file 118 | 119 | You can set parameter defaults with a region defaults file located in the ```~/.cfnparam directory,``` for example ```~/.cfnparam/.default```. If the region defaults file exists, then that file will be used for default values. This will override the existing default value in the template. The region names used will be the AWS API region name, for example: us-west-2, us-east-1, etc. 120 | 121 | If the region defaults file exists, you should see this message when running the ```create``` command: 122 | ```text 123 | Using region defaults file /Users/joeuser/.cfnparam/us-east-2.default for parameter defaults 124 | ``` 125 | 126 | You can create the region defaults file by first creating a paramaters file using the ```build``` command, then move or copy that parameters file to the region defaults file. 127 | 128 | #### Required parameters 129 | 130 | If a parameter is defined as required (```ConstraintDescription``` is used in the template), and you have not provided a value, you will see one of two messages. 131 | 132 | 1. If you ran ```cfnctl build``` you will see a message similar to this: 133 | 134 | ```text 135 | WARNING ONLY: Parameter "MyS3Bucket" is required but can be updated in parameters file and left empty for now 136 | ``` 137 | 138 | 2. If you ran ```cfnctl create``` you will see a message similar this: 139 | ```text 140 | MyS3Bucket []: 141 | REQUIREMENT: Parameter "MyS3Bucket" is required to create the stack, please enter a value, optionally exit create and rerun with build action 142 | MyS3Bucket: 143 | Required parameter MyS3Bucket not entered. The stack create will fail, but the parameters file will still be built. 144 | ``` 145 | 146 | After the build completes you will see this message, reminding you to update the value in the parameters file: 147 | 148 | ```text 149 | Some values are still needed, replace "" in /Users/joeuser/.cfnparam/My_Instance.json.default 150 | ``` 151 | 152 | In the parameters file you will see this, for ```MyS3Bucket```: 153 | 154 | ```text 155 | MyS3Bucket = 156 | ``` 157 | 158 | You will need to change `````` to a valid parameter to create the stack successfully. 159 | 160 | 161 | 162 | ### Optional (but recommended): Build cfnctl parameters file (stored in ~/.cfnparam/) 163 | 164 | When you run ```cfnctl build``` you will be prompted for each of the parameter values. The ```build``` process accounts for default values, and you will be double prompted on any parameter that is using ```ConstraintDescription``` and does not have a value. A file with parameters and the template location will be saved in the ```~/.cfnparam/``` directory with ```.default``` appended to the template name. For example, the default parameter file for the template named ```stack1.json```, is ```~/.cfnparam/stack1.json.default```. 165 | 166 | Here is an example of running the ```cfnctl build``` command: 167 | 168 | ```text 169 | $ cfnctl build -t My_Instance.json 170 | Using AWS credentials profile "default" 171 | Looks like we're in us-east-1 172 | Creating parameters file /Users/joeuser/.cfnparam/My_Instance.json.default 173 | EC2 keys found in us-east-1: 174 | Testing1 175 | Joeuser_IAD 176 | Select EC2 Key [Joeuser_IAD]: 177 | Getting VPC info... 178 | vpc-0000aaaa1111bbbb2 | 10.0.0.0/16 | False | test1-VPC 179 | vpc-aaaabbbbeebce1234 | 172.31.0.0/16 | True | default-vpc 180 | Select VPC [vpc-aaaabbbbeebce1234]: vpc-0000aaaa1111bbbb2 181 | Getting security groups for vpc-0000aaaa1111bbbb2 ... 182 | sg-1111aaaa3333bbbbcc | launch-wizard-1 183 | sg-2222bbbbcccc333334 | Ent-network 184 | sg-bbbccaa1234124efgh | default 185 | Select secuirty group [sg-bbbccaa1234124efgh]: 186 | MyInstanceType [m5.2xlarge]: t3.small 187 | OperatingSystem [centos7]: alinux2 188 | SshAccessCidr [111.222.333.444/32]: 333.333.444.444/32 189 | MyS3Bucket []: 190 | WARNING ONLY: Parameter "MyS3Bucket" is required but can be updated in parameters file and left empty for now 191 | MyS3Bucket: 192 | Getting subnets for vpc-0000aaaa1111bbbb2 ... 193 | subnet-aaaabbbcccbcd1234 | us-east-1b 194 | subnet-abbbcccbbbbcd12gg | us-east-1-bos-1a | Local Zone Subnet 195 | subnet-abbbcccbbbbcd1235 | us-east-1a 196 | subnet-addddbbbccbcd1232 | us-east-1f 197 | subnet-addddbbbccbcd1236 | us-east-1c 198 | subnet-addddbbbccbcd1238 | us-east-1e 199 | subnet-aaabbbbcccbcd1237 | us-east-1d 200 | Select subnet: [subnet-abbbcccbbbbcd1235]: 201 | UsePublicIp [true]: 202 | Some values are still needed, replace "" in /Users/joeuser/.cfnparam/My_Instance.json.default 203 | Done building cfnctl parameters file /Users/joeuser/.cfnparam/My_Instance.json.default, includes template location 204 | ``` 205 | 206 | #### Edit the parameters file, and fill in values as needed 207 | 208 | After running the ```cfnctl build```, in your home directory under ```~/.cfnparam/```, you will find a parameters file. Edit the parameters file as needed, if you see ``````, those values can not be null for a successful stack luanch. 209 | 210 | Example parameters file: 211 | 212 | ```text 213 | [AWS-Config] 214 | TemplateBody = /Users/joeuser/templates/My_Instance.json 215 | 216 | [Paramters] 217 | EC2KeyName = Joeuser_IAD 218 | ExistingSecurityGroup = sg-bbbccaa1234124efgh 219 | MyInstanceType = t3.small 220 | OperatingSystem = alinux2 221 | SshAccessCidr = 333.333.444.444/32 222 | SshBucket = 223 | Subnet = subnet-abbbcccbbbbcd1235 224 | UsePublicIp = true 225 | VpcId = vpc-aaaabbbbeebce1234 226 | ``` 227 | 228 | 229 | ### Create the stack 230 | 231 | The stack can be created in two ways, either with the ```-t``` flag or the ```-f``` flag 232 | 233 | #### 1. Create with the ```-t``` flag 234 | 235 | If you already created the parameters file (steps above), when you run the create command you will be prompted to choose from either the existing (default) parameters file, or continue the create while answering the parameters questions again. 236 | 237 | For example, if the default parameters file exists you will see this: 238 | 239 | ``` 240 | $ cfnctl create -n teststack1 -t My_Instance.json 241 | Using AWS credentials profile "default" 242 | Looks like we're in us-east-1 243 | Default parameters file /Users/joeuser/.cfnparam/My_Instance.json.default exists, use this file [Y/n]: 244 | ``` 245 | Answering "y" will create the stack using the values from the previously created default parameters file. 246 | 247 | If you answer "n", you will be prompted for each parameter, and the parameters will be saved in the ```~/.cfnparam/``` directory with the values used to create the stack. The parameters file name is the stack name appended to the template file name. For example, the parameters file for the template named ```My_Instance.json``` when specifying the stack name ```teststack1```, will be ```~/.cfnparam/My_Instance.json.teststack1```: 248 | 249 | ```text 250 | $ cfnctl create -n teststack1 -t My_Instance.json 251 | Using AWS credentials profile "default" 252 | Looks like we're in us-east-1 253 | Default parameters file /Users/joeuser/.cfnparam/My_Instance.json.default exists, use this file [Y/n]: n 254 | Stack parameters file does not exists, continuing... 255 | Creating parameters file /Users/joeuser/.cfnparam/My_Instance.json.teststack1 256 | ... 257 | ``` 258 | 259 | #### 2. Create with the ```-f``` flag 260 | 261 | You can use any parameters file (using ```-f```) to create a stack, as the parameters file has the template location and the paramters. 262 | 263 | For example, to create a new stack (teststack2) using all the parameters from the previously created stack named ```teststack1```, you run this: 264 | 265 | ``` 266 | $ cfnctl create -n teststack2 -f ~/.cfnparam/My_Instance.json.teststack1 267 | ``` 268 | 269 | #### Example ```cfnctl create``` using the default parameters file (from ```cfnctl build```): 270 | 271 | Here is example output from a ```cfnctl create```, using the previously created default parameters file. The status of the stack, the parameters used, and the output(s) are also displayed: 272 | 273 | ``` 274 | $ cfnctl create -n teststack1 -t My_Instance.json 275 | Using AWS credentials profile "default" 276 | Looks like we're in us-east-1 277 | Default parameters file /Users/joeuser/.cfnparam/My_Instance.json.default exists, use this file [Y/n]: 278 | Using parameters file: /Users/joeuser/.cfnparam/My_Instance.json.default 279 | Using template file: /Users/joeuser/templates/My_Instance.json 280 | Attempting to launch teststack1 281 | teststack1 : CREATE_IN_PROGRESS : User Initiated 282 | SshSecurityGroup : CREATE_IN_PROGRESS 283 | InstanceWaitHandle : CREATE_IN_PROGRESS 284 | RootRole : CREATE_IN_PROGRESS 285 | InstanceWaitHandle : CREATE_IN_PROGRESS : Resource creation Initiated 286 | InstanceWaitHandle : CREATE_COMPLETE 287 | RootRole : CREATE_IN_PROGRESS : Resource creation Initiated 288 | SshSecurityGroup : CREATE_IN_PROGRESS : Resource creation Initiated 289 | SshSecurityGroup : CREATE_COMPLETE 290 | RootRole : CREATE_COMPLETE 291 | RootInstanceProfile : CREATE_IN_PROGRESS 292 | RootInstanceProfile : CREATE_IN_PROGRESS : Resource creation Initiated 293 | RootInstanceProfile : CREATE_COMPLETE 294 | MyInstance : CREATE_IN_PROGRESS 295 | MyInstance : CREATE_IN_PROGRESS : Resource creation Initiated 296 | MyInstance : CREATE_COMPLETE 297 | InstanceWaitCondition : CREATE_IN_PROGRESS 298 | InstanceWaitCondition : CREATE_IN_PROGRESS : Resource creation Initiated 299 | InstanceWaitCondition : CREATE_COMPLETE 300 | teststack1 : CREATE_COMPLETE 301 | 302 | Status: 303 | teststack1 2021-09-16 14:35:11 CREATE_COMPLETE test instance launch 304 | 305 | [Parameters] 306 | ExistingSecurityGroup = sg-bbbccaa1234124efgh 307 | OperatingSystem = alinux2 308 | VpcId = vpc-aaaabbbbeebce1234 309 | UsePublicIp = true 310 | SshAccessCidr = 333.333.444.444/32 311 | EC2KeyName = Joeuser_IAD 312 | MyInstanceType = t3.small 313 | Subnet = subnet-abbbcccbbbbcd1235 314 | 315 | [Outputs] 316 | InstanceID = i-00011100022200333 317 | InstancePublicIP = 54.12.11.13 318 | InstancePrivateIP = 172.25.5.5 319 | ``` 320 | 321 | ## Change Log 322 | 323 | 324 | ## ToDo list 325 | 326 | * Add catch-all for help 327 | 328 | -------------------------------------------------------------------------------- /_codebuild/Buildspec: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | #env: 4 | #variables: 5 | # key: "value" 6 | # key: "value" 7 | #parameter-store: 8 | # key: "value" 9 | # key: "value" 10 | 11 | phases: 12 | #install: 13 | #commands: 14 | # - command 15 | # - command 16 | #finally: 17 | # - command 18 | # - command 19 | #pre_build: 20 | #commands: 21 | # - command 22 | # - command 23 | #finally: 24 | # - command 25 | # - command 26 | build: 27 | commands: 28 | - cd aws-cfn-control 29 | - pip install -e . 30 | - cd - 31 | - curl 169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI 32 | - echo "Installing jq..." 33 | - curl -qL -o jq https://stedolan.github.io/jq/download/linux64/jq && chmod +x ./jq 34 | - echo "Configuring AWS credentials" 35 | - curl -qL -o aws_credentials.json http://169.254.170.2/$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI > aws_credentials.json 36 | - aws configure set region $AWS_REGION 37 | - aws configure set aws_access_key_id `./jq -r '.AccessKeyId' aws_credentials.json` 38 | - aws configure set aws_secret_access_key `./jq -r '.SecretAccessKey' aws_credentials.json` 39 | - aws configure set aws_session_token `./jq -r '.Token' aws_credentials.json` 40 | - cfnctl -h 41 | - cfnctl -a 42 | #finally: 43 | # - command 44 | # - command 45 | #post_build: 46 | #commands: 47 | # - command 48 | # - command 49 | #finally: 50 | # - command 51 | # - command 52 | #artifacts: 53 | #files: 54 | # - location 55 | # - location 56 | # - name 57 | #discard-paths: yes 58 | #base-directory: location 59 | #cache: 60 | #paths: 61 | # - path 62 | # - path 63 | -------------------------------------------------------------------------------- /_images/deploy_to_aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-cfn-control/29e5d602a82b66ddeca3b6761587804a23c6ae3d/_images/deploy_to_aws.png -------------------------------------------------------------------------------- /_tests/test.sh: -------------------------------------------------------------------------------- 1 | echo "testing complete" 2 | -------------------------------------------------------------------------------- /aws-cfn-control/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.md -------------------------------------------------------------------------------- /aws-cfn-control/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /aws-cfn-control/awscfnctl/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 5 | # except in compliance with the License. A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 10 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations under the License. 12 | # 13 | 14 | from .awscfnctl import CfnControl 15 | -------------------------------------------------------------------------------- /aws-cfn-control/awscfnctl/add_ena_vfi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | # 4 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 7 | # except in compliance with the License. A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 12 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under the License. 14 | # 15 | 16 | ena_version="1.2.0" 17 | vfi_version="4.2.1" 18 | 19 | function create_vfi_dkms_file { 20 | 21 | touch /usr/src/ixgbevf-${vfi_version}/dkms.conf 22 | 23 | cat << EOF >> /usr/src/ixgbevf-${vfi_version}/dkms.conf 24 | PACKAGE_NAME="ixgbevf" 25 | PACKAGE_VERSION=$vfi_version 26 | CLEAN="cd src/; make clean" 27 | MAKE="cd src/; make BUILD_KERNEL=\${kernelver}" 28 | BUILT_MODULE_LOCATION[0]="src/" 29 | BUILT_MODULE_NAME[0]="ixgbevf" 30 | DEST_MODULE_LOCATION[0]="/updates" 31 | DEST_MODULE_NAME[0]="ixgbevf" 32 | AUTOINSTALL="yes" 33 | EOF 34 | 35 | } 36 | 37 | function create_ena_dkms_file { 38 | 39 | touch /usr/src/amzn-drivers-${ena_version}/dkms.conf 40 | 41 | cat << EOF >> /usr/src/amzn-drivers-${ena_version}/dkms.conf 42 | PACKAGE_NAME="ena" 43 | PACKAGE_VERSION="$ena_version" 44 | CLEAN="make -C kernel/linux/ena clean" 45 | MAKE="make -C kernel/linux/ena/ BUILD_KERNEL=\${kernelver}" 46 | BUILT_MODULE_NAME[0]="ena" 47 | BUILT_MODULE_LOCATION="kernel/linux/ena" 48 | DEST_MODULE_LOCATION[0]="/updates" 49 | DEST_MODULE_NAME[0]="ena" 50 | AUTOINSTALL="yes" 51 | EOF 52 | 53 | } 54 | 55 | function build_inst_vfi { 56 | 57 | pushd /tmp 58 | curl -O https://s3.amazonaws.com/${bucket}/ixgbevf-${vfi_version}.tar.gz 59 | tar -xvf ixgbevf-${vfi_version}.tar.gz 60 | sudo mv ixgbevf-${vfi_version} /usr/src 61 | 62 | create_vfi_dkms_file 63 | 64 | sudo dkms add -m ixgbevf -v $vfi_version 65 | sudo dkms build -m ixgbevf -v $vfi_version 66 | sudo dkms install -m ixgbevf -v $vfi_version 67 | 68 | popd 69 | 70 | } 71 | 72 | function build_inst_ena { 73 | 74 | pushd /tmp 75 | if [[ ! -d amzn-drivers ]]; then 76 | git clone https://github.com/amzn/amzn-drivers 77 | fi 78 | 79 | sudo mv amzn-drivers /usr/src/amzn-drivers-${ena_version} 80 | 81 | create_ena_dkms_file 82 | 83 | sudo dkms add -m amzn-drivers -v $ena_version 84 | sudo dkms build -m amzn-drivers -v $ena_version 85 | sudo dkms install -m amzn-drivers -v $ena_version 86 | 87 | popd 88 | } 89 | 90 | function fix_net_dev_names { 91 | 92 | sudo sed -i '/^GRUB\_CMDLINE\_LINUX/s/\"$/\ net\.ifnames\=0\"/' /etc/default/grub 93 | sudo grub2-mkconfig -o /boot/grub2/grub.cfg 94 | 95 | } 96 | 97 | function install_reqs { 98 | 99 | sudo yum install "kernel-devel-uname-r == $(uname -r)" -y 100 | sudo yum install vim git gcc dkms -y 101 | 102 | } 103 | 104 | function ck_os { 105 | 106 | os=$(cat /etc/redhat-release | awk {'print $1'} | tr '[A-Z]' '[a-z]') 107 | 108 | } 109 | 110 | function ck_status { 111 | 112 | vfi_status=$(sudo dkms -m ixgbevf status | grep installed) 113 | ena_status=$(sudo dkms -m amzn-drivers status | grep installed) 114 | 115 | if [[ "$vfi_status" ]]; then 116 | echo "VFI (ixdbevf) drivers are installed" 117 | else 118 | echo "VFI (ixdbevf) drivers are NOT installed" 119 | fi 120 | 121 | if [[ "$ena_status" ]]; then 122 | echo "ENA (amzn-drivers) drivers are installed" 123 | else 124 | echo "ENA (amzn-drivers) drivers are NOT installed" 125 | fi 126 | 127 | } 128 | 129 | 130 | install_reqs 131 | build_inst_ena 132 | build_inst_vfi 133 | fix_net_dev_names 134 | sudo depmod 135 | ck_status 136 | 137 | 138 | -------------------------------------------------------------------------------- /aws-cfn-control/awscfnctl/asgctl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 7 | # except in compliance with the License. A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 12 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under the License. 14 | # 15 | 16 | import sys 17 | import argparse 18 | from awscfnctl import CfnControl 19 | 20 | progname = 'asgctl' 21 | 22 | def arg_parse(): 23 | 24 | parser = argparse.ArgumentParser(prog=progname, 25 | description='Control the instances in an ASG', 26 | epilog='Example: {} -a -r '.format(progname) 27 | ) 28 | 29 | opt_group = parser.add_argument_group('optional arguments') 30 | opt_group.add_argument('-r', dest='region', required=False, help="Region name") 31 | 32 | req_group = parser.add_argument_group('required arguments') 33 | req_group.add_argument('action', help='Action to take: ' 34 | 'status, enter-stby, exit-stby, stop, start (stop will enter standby first, ' 35 | 'and start will exit standby after start is complete') 36 | req_group.add_argument('-a', dest='asg', required=True) 37 | 38 | return parser.parse_args() 39 | 40 | 41 | def main(): 42 | 43 | args = arg_parse() 44 | 45 | region = args.region 46 | asg = args.asg 47 | action = args.action 48 | 49 | i = CfnControl(region=region, asg=asg) 50 | 51 | if action == 'enter-stby': 52 | i.asg_enter_standby() 53 | elif action == 'stop': 54 | i.asg_enter_standby() 55 | i.stop_instances() 56 | elif action == 'start': 57 | i.start_instances() 58 | i.asg_exit_standby() 59 | elif action == 'exit-stby': 60 | i.asg_exit_standby() 61 | elif action == 'status': 62 | i.ck_asg_status() 63 | i.ck_inst_status() 64 | 65 | if __name__ == "__main__": 66 | try: 67 | sys.exit(main()) 68 | except KeyboardInterrupt: 69 | print('\nReceived Keyboard interrupt.') 70 | print('Exiting...') 71 | except ValueError as e: 72 | print('ERROR: {0}'.format(e)) 73 | 74 | -------------------------------------------------------------------------------- /aws-cfn-control/awscfnctl/awscfnctl.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 5 | # except in compliance with the License. A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 10 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations under the License. 12 | # 13 | 14 | 15 | import os 16 | import sys 17 | import time 18 | import json 19 | import errno 20 | import boto3 21 | import botocore 22 | import operator 23 | import textwrap 24 | import subprocess 25 | import configparser 26 | from urllib.parse import urlparse 27 | from botocore.exceptions import ClientError 28 | from botocore.exceptions import EndpointConnectionError 29 | from cfn_flip import flip, to_yaml, to_json 30 | 31 | 32 | class CfnControl: 33 | 34 | def __init__(self, **kwords): 35 | 36 | """ 37 | Main class init 38 | 39 | :param kwords: aws_profile, region, asg, param_file, instances, cfn_action 40 | 41 | aws_profile: If this is not given, then search 42 | the session, otherwise use "default" 43 | 44 | region: If this is not given, then search the session, 45 | if it's not in the session the raise error. 46 | 47 | asg: If an ASG name is given, append the instances 48 | from the ASG to the instance list 49 | 50 | param_file: Location of the the parameter files for 51 | cfnctl command (~/.cfnparam) 52 | 53 | instances: list() of instances 54 | 55 | cfn_action: Action: build|create|list|delete 56 | 57 | """ 58 | 59 | self.cfn_action = kwords.get('cfn_action') 60 | 61 | self.aws_profile = kwords.get('aws_profile') 62 | if not self.aws_profile: 63 | self.aws_profile = 'default' 64 | elif self.aws_profile == None: 65 | self.aws_profile = 'default' 66 | 67 | print('Using AWS credentials profile "{0}"'.format(self.aws_profile)) 68 | 69 | self.session = boto3.session.Session(profile_name=self.aws_profile) 70 | self.region = kwords.get('region') 71 | 72 | if not self.region and not self.session.region_name: 73 | errmsg = "Must specify a region, either at the command (-r) or in your AWS CLI config" 74 | raise ValueError(errmsg) 75 | 76 | if not self.region: 77 | self.region = self.session.region_name 78 | 79 | print("Looks like we're in {0}".format(self.region)) 80 | 81 | # boto resources 82 | self.s3 = self.session.resource('s3') 83 | self.ec2 = self.session.resource('ec2', region_name=self.region) 84 | 85 | # test api connection 86 | try: 87 | for bucket in self.s3.buckets.all(): 88 | pass 89 | except botocore.exceptions.NoCredentialsError as e: 90 | print(e, '"' + self.aws_profile + '", exiting...') 91 | sys.exit(1) 92 | 93 | 94 | # boto clients 95 | self.client_ec2 = self.session.client('ec2', region_name=self.region) 96 | self.client_asg = self.session.client('autoscaling', region_name=self.region) 97 | self.client_cfn = self.session.client('cloudformation', region_name=self.region) 98 | self.client_s3 = self.session.client('s3', region_name=self.region) 99 | 100 | # grab passed arguments 101 | self.asg = kwords.get('asg') 102 | self.cfn_param_file = kwords.get('param_file') 103 | 104 | # get instances passed as an argument 105 | self.instances = list() 106 | try: 107 | if kwords.get('instances'): 108 | self.instances = kwords.get('instances') 109 | except Exception as e: 110 | raise ValueError(e) 111 | 112 | # Stack variables 113 | # 114 | self.stack_name = None 115 | self.template = None 116 | self.TemplateURL = None 117 | self.TemplateBody = None 118 | self.vpc_variable_name = None 119 | 120 | # Set user directory and current directory 121 | # 122 | self.my_cwd = os.path.curdir 123 | self.homedir = os.path.expanduser("~") 124 | 125 | # Use user directory to build the cfnparam file location 126 | # current default is ~/.cfnparm 127 | # 128 | self.param_file_list = None 129 | self.cfn_param_file_values = dict() 130 | self.cfn_param_file_basename = None 131 | self.cfn_param_base_dir = ".cfnparam" 132 | self.cfn_param_file_dir = os.path.join(self.homedir, self.cfn_param_base_dir) 133 | 134 | ## For future release 135 | ## Check for global defaults file 136 | ## 137 | #global_default_file = os.path.join(os.path.join(self.cfn_param_file_dir, self.region + ".default")) 138 | #if os.path.isfile(global_default_file): 139 | # self.global_default_file = global_default_file 140 | # print("Found global default file {0}".format(self.global_default_file)) 141 | 142 | # Define other variables 143 | # 144 | self.vpc_id = None 145 | self.template_url = None 146 | self.template_body = None 147 | self.key_pairs = list() 148 | 149 | # Set message level 150 | self.INFO_LEVEL = 1 151 | 152 | # First API call - grab key pairs, this will determine if we can talk to the API 153 | # 154 | try: 155 | key_pairs_response = self.client_ec2.describe_key_pairs() 156 | for pair in (key_pairs_response['KeyPairs']): 157 | self.key_pairs.append(pair['KeyName']) 158 | except EndpointConnectionError as e: 159 | errmsg = "Please make sure that the region specified ({0}) is valid\n".format(self.region) 160 | raise ValueError(errmsg + str(e)) 161 | except botocore.exceptions.NoCredentialsError as e: 162 | pass 163 | except Exception as e: 164 | raise ValueError(e) 165 | 166 | # For some lists, we only want to print out certain keys: 167 | # 168 | self.vpc_keys_to_print = ['Tag_Name', 169 | 'IsDefault', 170 | 'CidrBlock', 171 | ] 172 | 173 | self.subnet_keys_to_print = ['Tag_Name', 174 | 'AvailabilityZone', 175 | ] 176 | 177 | self.sec_groups_keys_to_print = ['Description', 178 | 'GroupName', 179 | ] 180 | 181 | # If the `asg` keyword was passed, then build an instance list from the ASG 182 | # 183 | if self.asg: 184 | response = self.client_asg.describe_auto_scaling_groups(AutoScalingGroupNames=[self.asg]) 185 | 186 | print('Gathering instances from ASG {0}'.format(self.asg)) 187 | 188 | # Build instance IDs list 189 | for r in response['AutoScalingGroups']: 190 | for i in r['Instances']: 191 | self.instances.append(self.ec2.Instance(i['InstanceId']).instance_id) 192 | 193 | if not self.instances: 194 | print("Instance list is null, continuing...") 195 | 196 | # Use the region default file if it exists 197 | # Default: ~/.cfnparam/.default, e.g. ~/.cfnparam/us-west-2.default 198 | # 199 | self.region_defaults = self.cfn_param_file_dir + '/' + self.region + '.default' 200 | if os.path.exists(self.region_defaults) and self.INFO_LEVEL: 201 | if self.cfn_action == "build" or self.cfn_action == "create": 202 | print('Using region defaults file', self.region_defaults, 'for parameter defaults') 203 | self.INFO_LEVEL = 0 204 | 205 | @staticmethod 206 | def runcmd(cmdlist): 207 | """ 208 | runs a command 209 | 210 | :param cmdlist: command to run 211 | :return: If there is an error, returns stdout and stderr, otherwise just stdout 212 | """ 213 | 214 | proc = subprocess.Popen(cmdlist, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 215 | out, err = proc.communicate() 216 | if proc.returncode != 0: 217 | log = out + err 218 | else: 219 | log = out 220 | 221 | return log, proc.returncode 222 | 223 | def instanace_list(self): 224 | """ 225 | returns list() of instance IDs 226 | """ 227 | return self.instances 228 | 229 | @staticmethod 230 | def inst_list_from_file(file_name): 231 | """ 232 | reads a list from a named file 233 | 234 | :param file_name: a file name 235 | :return: a list spearated but "\n" 236 | """ 237 | try: 238 | f = open(file_name, "r") 239 | except: 240 | raise 241 | 242 | s = f.read() 243 | f.close() 244 | # if last char == '\n', delete it 245 | if s[-1] == "\n": 246 | s = s[:-1] 247 | l = s.split("\n") 248 | return l 249 | 250 | def get_asg_from_stack(self, stack_name=None): 251 | 252 | # returns a list of ASG names for a given stack 253 | 254 | self.asg = list() 255 | 256 | if stack_name is None: 257 | stack_name = self.stack_name 258 | 259 | # Debug 260 | # print('Getting ASG name(s) from stack {0} (returns a list)'.format(stack_name)) 261 | 262 | try: 263 | stk_response = self.client_cfn.describe_stack_resources(StackName=stack_name) 264 | except ClientError as e: 265 | raise ValueError(e) 266 | 267 | for resp in stk_response['StackResources']: 268 | for resrc_type in resp: 269 | if resrc_type == "ResourceType": 270 | if resp[resrc_type] == "AWS::AutoScaling::AutoScalingGroup": 271 | self.asg.append(resp['PhysicalResourceId']) 272 | 273 | return self.asg 274 | 275 | def get_inst_from_asg(self, asg=None): 276 | 277 | if asg is None: 278 | asg = self.asg 279 | 280 | # Debug 281 | # print('Getting ASG instances from {0}'.format(asg)) 282 | 283 | if type(asg) is list: 284 | response = self.client_asg.describe_auto_scaling_groups(AutoScalingGroupNames=asg) 285 | else: 286 | response = self.client_asg.describe_auto_scaling_groups(AutoScalingGroupNames=[asg]) 287 | 288 | self.instances = list() 289 | # Build instance IDs list 290 | for r in response['AutoScalingGroups']: 291 | for i in r['Instances']: 292 | self.instances.append(self.ec2.Instance(i['InstanceId']).instance_id) 293 | 294 | return self.instances 295 | 296 | def asg_enter_standby(self, instances=None): 297 | 298 | sleep_time = 10 299 | print("Setting instances to ASG standby") 300 | 301 | if instances is None: 302 | instances = self.instances 303 | 304 | response = self.client_asg.enter_standby(InstanceIds=instances, AutoScalingGroupName=self.asg, 305 | ShouldDecrementDesiredCapacity=True 306 | ) 307 | 308 | print("Sleeping for {0} seconds to allow for instances to enter standby".format(sleep_time)) 309 | time.sleep(sleep_time) 310 | 311 | return response 312 | 313 | def asg_exit_standby(self, instances=None): 314 | 315 | sleep_time = 30 316 | print("Instances are exiting from ASG standby") 317 | 318 | if instances is None: 319 | instances = self.instances 320 | 321 | response = self.client_asg.exit_standby(InstanceIds=instances, AutoScalingGroupName=self.asg, ) 322 | 323 | print("Sleeping for {0} seconds to allow for instances to exit standby".format(sleep_time)) 324 | time.sleep(sleep_time) 325 | 326 | return response 327 | 328 | def stop_instances(self, instances=None): 329 | 330 | sleep_time = 300 331 | print("Stopping instances") 332 | 333 | if instances is None: 334 | instances = self.instances 335 | 336 | response = self.client_ec2.stop_instances(InstanceIds=instances, DryRun=False) 337 | print("Sleeping for {0} seconds to allow for instances to stop".format(sleep_time)) 338 | time.sleep(sleep_time) 339 | 340 | return response 341 | 342 | def start_instances(self, instances=None): 343 | 344 | sleep_time = 120 345 | print("Starting instances") 346 | 347 | if instances is None: 348 | instances = self.instances 349 | 350 | response = self.client_ec2.start_instances(InstanceIds=instances, DryRun=False) 351 | print("Sleeping for {0} seconds to allow for instances to start".format(sleep_time)) 352 | time.sleep(sleep_time) 353 | 354 | return response 355 | 356 | def terminate_instances(self, instances=None): 357 | 358 | sleep_time = 120 359 | print("Terminating instances") 360 | 361 | if instances is None: 362 | instances = self.instances 363 | 364 | response = self.client_ec2.terminate_instances(InstanceIds=instances, DryRun=False) 365 | print("Sleeping for {0} seconds to allow for instances to terminate".format(sleep_time)) 366 | time.sleep(sleep_time) 367 | 368 | return response 369 | 370 | def ck_inst_status(self): 371 | 372 | response = self.client_ec2.describe_instance_status(InstanceIds=self.instances, IncludeAllInstances=True) 373 | 374 | running = list() 375 | not_running = list() 376 | 377 | for r in response['InstanceStatuses']: 378 | if (r['InstanceState']['Name']) == 'running': 379 | running.append(r['InstanceId']) 380 | else: 381 | not_running.append(r['InstanceId']) 382 | 383 | print("Instance Info:") 384 | print(" {0:3d} instances are running".format(len(running))) 385 | print(" {0:3d} instances are not running".format(len(not_running))) 386 | 387 | def ck_asg_inst_status(self, asg=None): 388 | 389 | if asg is None: 390 | asg = self.asg 391 | 392 | response = self.client_asg.describe_auto_scaling_groups(AutoScalingGroupNames=[asg]) 393 | in_service = list() 394 | not_in_service = list() 395 | 396 | # Build instance IDs list 397 | for r in response['AutoScalingGroups']: 398 | for i in r['Instances']: 399 | if self.ec2.Instance(i['LifecycleState']).instance_id == 'InService': 400 | in_service.append(self.ec2.Instance(i['InstanceId']).instance_id) 401 | else: 402 | not_in_service.append(self.ec2.Instance(i['InstanceId']).instance_id) 403 | 404 | print("ASG instances status:") 405 | print(" {0:3d} InService".format(len(in_service))) 406 | print(" {0:3d} Not InService".format(len(not_in_service))) 407 | 408 | return in_service, not_in_service 409 | 410 | def enable_ena_vfi(self, instances=None): 411 | 412 | if instances is None: 413 | all_instances = self.instances 414 | else: 415 | all_instances = instances 416 | 417 | inst_add_ena_vfi = list() 418 | 419 | print("Checking if instances are ENA/VFI enabled") 420 | 421 | for inst_id in all_instances: 422 | 423 | response_vfi = self.client_ec2.describe_instance_attribute( 424 | Attribute='sriovNetSupport', 425 | InstanceId=inst_id 426 | ) 427 | 428 | try: 429 | if (response_vfi['SriovNetSupport']['Value']) == 'simple': 430 | pass 431 | except KeyError: 432 | if inst_id not in inst_add_ena_vfi: 433 | inst_add_ena_vfi.append(inst_id) 434 | 435 | # Attribute='enaSupport' is not currently supported 436 | 437 | if not inst_add_ena_vfi: 438 | print("All instances are VFI enabled (Can't check for ENA)") 439 | return 440 | 441 | print("Enabling ENA and VFI on instances") 442 | 443 | self.instances = inst_add_ena_vfi 444 | 445 | (inst_in_service, inst_in_standby) = self.ck_asg_inst_status() 446 | 447 | if inst_in_service: 448 | self.asg_enter_standby(inst_in_service) 449 | 450 | self.stop_instances() 451 | 452 | response_ec2_vfi = None 453 | response_ec2_ena = None 454 | 455 | for inst_id in self.instances: 456 | print('Enabling ENA/VFI on ' + inst_id) 457 | response_ec2_vfi = self.client_ec2.modify_instance_attribute(InstanceId=inst_id, 458 | SriovNetSupport={'Value': 'simple'} 459 | ) 460 | response_ec2_ena = self.client_ec2.modify_instance_attribute(InstanceId=inst_id, 461 | EnaSupport={'Value': True}, 462 | ) 463 | 464 | self.start_instances() 465 | self.asg_exit_standby() 466 | if instances is None: 467 | self.instances = all_instances 468 | 469 | return response_ec2_vfi, response_ec2_ena 470 | 471 | def get_param_files(self, os_dir): 472 | 473 | if os_dir is None: 474 | os_dir = self.cfn_param_file_dir 475 | 476 | try: 477 | self.param_file_list = os.listdir(os_dir) 478 | return self.param_file_list 479 | except Exception as e: 480 | raise ValueError(e) 481 | 482 | def read_cfn_param_file(self, cfn_param_file=None): 483 | 484 | parser = configparser.SafeConfigParser() 485 | parser.optionxform = str 486 | 487 | if not cfn_param_file: 488 | cfn_param_file = self.cfn_param_file 489 | 490 | if os.path.isfile(cfn_param_file): 491 | if self.INFO_LEVEL: 492 | print("Using parameters file: {0}".format(cfn_param_file)) 493 | parser.read(cfn_param_file) 494 | elif os.path.isfile(os.path.join(self.cfn_param_file_dir, cfn_param_file + ".json.cf")): 495 | if self.INFO_LEVEL: 496 | print("Using parameters file: {0}".format( 497 | os.path.join(self.cfn_param_file_dir, cfn_param_file + ".json.cf")) 498 | ) 499 | parser.read(os.path.join(self.cfn_param_file_dir, cfn_param_file + ".json.cf")) 500 | elif os.path.isfile(os.path.join(self.cfn_param_file_dir, cfn_param_file)): 501 | if self.INFO_LEVEL: 502 | print("Using parameters file: {0}".format( 503 | os.path.join(self.cfn_param_file_dir, cfn_param_file)) 504 | ) 505 | parser.read(os.path.join(self.cfn_param_file_dir, cfn_param_file)) 506 | elif cfn_param_file == "NO_PARAM_FILE": 507 | return None 508 | else: 509 | errmsg = "Config file {0} not found".format(cfn_param_file) 510 | raise ValueError(errmsg) 511 | 512 | params = list() 513 | 514 | boolean_keys = ['EnableEnaVfi', 515 | 'AddNetInterfaces', 516 | 'CreateElasticIP' 517 | ] 518 | 519 | not_cfn_param_keys = ['EnableEnaVfi', 520 | 'AddNetInterfaces', 521 | 'TotalNetInterfaces', 522 | 'TemplateURL', 523 | 'TemplateBody' 524 | ] 525 | 526 | for section_name in parser.sections(): 527 | for key, value in parser.items(section_name): 528 | #print('key: {0}'.format(key)) 529 | if key in boolean_keys: 530 | value = parser.getboolean(section_name, key) 531 | 532 | if key in not_cfn_param_keys: 533 | self.cfn_param_file_values[key] = value 534 | else: 535 | self.cfn_param_file_values[key] = value 536 | params.append( 537 | { 538 | 'ParameterKey': key, 539 | 'ParameterValue': str(value), 540 | 'UsePreviousValue': False 541 | } 542 | ) 543 | 544 | return params 545 | 546 | @staticmethod 547 | def url_check(url): 548 | try: 549 | result = urlparse.urlparse(url) 550 | return result.scheme and result.netloc and result.path 551 | except: 552 | return False 553 | 554 | def cr_stack(self, stack_name, cfn_param_file, verbose=False, set_rollback='ROLLBACK', template=None): 555 | """ 556 | Three steps: 557 | 558 | 1. Validate template 559 | 2. Build parameters file 560 | 3. Launch Stack 561 | 562 | :param stack_name: 563 | :param cfn_param_file: 564 | :param verbose: 565 | :param set_rollback: 566 | :param template: 567 | :return: 568 | """ 569 | 570 | 571 | response = None 572 | 573 | try: 574 | stk_response = self.client_cfn.describe_stacks(StackName=stack_name) 575 | print('The stack "{0}" exists. Exiting...'.format(stack_name)) 576 | sys.exit() 577 | except ValueError as e: 578 | raise ValueError 579 | except ClientError as e: 580 | pass 581 | 582 | if template is not None: 583 | # check if the template is a URL, or a local file 584 | if self.url_check(template): 585 | self.template_url = template 586 | self.validate_cfn_template(template_url=self.template_url) 587 | if not cfn_param_file: 588 | cfn_param_file = self.build_cfn_param(stack_name, self.template_url, cli_template=template, verbose=verbose) 589 | else: 590 | template_path = os.path.abspath(template) 591 | self.validate_cfn_template(template_body=template_path) 592 | if not cfn_param_file: 593 | cfn_param_file = self.build_cfn_param(stack_name, template_path, cli_template=template, verbose=verbose) 594 | self.template_body = self.parse_cfn_template(template_path) 595 | 596 | cfn_params = self.read_cfn_param_file(cfn_param_file) 597 | self.cfn_param_file = cfn_param_file 598 | 599 | print("Attempting to launch {}".format(stack_name)) 600 | 601 | cfn_param_file_location = None 602 | template_tags = [ 603 | { 604 | 'Key': 'Name', 605 | 'Value': stack_name 606 | }, 607 | { 608 | 'Key': 'cfnctl_param_file', 609 | 'Value': cfn_param_file_location 610 | }, 611 | ] 612 | 613 | if self.cfn_param_file == "NO_PARAM_FILE": 614 | # there is no parameters file (cfn_param_fie == "NO_PARAM_FILE") 615 | 616 | # set the location of cfnctl_param_file 617 | template_tags[1]['Value'] = self.cfn_param_file 618 | try: 619 | if self.template_url: 620 | response = self.client_cfn.create_stack( 621 | StackName=stack_name, 622 | TemplateURL=self.template_url, 623 | TimeoutInMinutes=600, 624 | Capabilities=['CAPABILITY_IAM'], 625 | OnFailure=set_rollback, 626 | Tags=template_tags 627 | ) 628 | elif self.template_body: 629 | response = self.client_cfn.create_stack( 630 | StackName=stack_name, 631 | TemplateBody=self.template_body, 632 | TimeoutInMinutes=600, 633 | Capabilities=['CAPABILITY_IAM'], 634 | OnFailure=set_rollback, 635 | Tags=template_tags 636 | ) 637 | except ClientError as e: 638 | print(e.response['Error']['Message']) 639 | return 640 | 641 | else: 642 | # The parameters file exists 643 | try: 644 | if self.cfn_param_file_values['TemplateURL']: 645 | self.template_url = self.cfn_param_file_values['TemplateURL'] 646 | print("Using template from URL: {}".format(self.template_url)) 647 | except Exception as e: 648 | if "TemplateURL" in str(e): 649 | try: 650 | if self.cfn_param_file_values['TemplateBody']: 651 | self.template_body = self.cfn_param_file_values['TemplateBody'] 652 | print("Using template file: {}".format(self.template_body)) 653 | self.template_body = self.parse_cfn_template(self.template_body) 654 | except Exception as e: 655 | raise ValueError(e) 656 | else: 657 | raise ValueError(e) 658 | 659 | # set the location of cfnctl_param_file 660 | template_tags[1]['Value'] = os.path.basename(self.cfn_param_file) 661 | try: 662 | if self.template_url: 663 | response = self.client_cfn.create_stack( 664 | StackName=stack_name, 665 | TemplateURL=self.template_url, 666 | Parameters=cfn_params, 667 | TimeoutInMinutes=600, 668 | Capabilities=['CAPABILITY_IAM'], 669 | OnFailure=set_rollback, 670 | Tags=template_tags 671 | ) 672 | elif self.template_body: 673 | response = self.client_cfn.create_stack( 674 | StackName=stack_name, 675 | TemplateBody=self.template_body, 676 | Parameters=cfn_params, 677 | TimeoutInMinutes=600, 678 | Capabilities=['CAPABILITY_IAM'], 679 | OnFailure=set_rollback, 680 | Tags=template_tags 681 | ) 682 | except ClientError as e: 683 | print(e.response['Error']['Message']) 684 | return 685 | 686 | stack_rc = self.stack_status(stack_name=stack_name) 687 | 688 | if stack_rc != 'CREATE_COMPLETE': 689 | print('Stack creation failed with {0}'.format(stack_rc)) 690 | return 691 | 692 | self.asg = self.get_asg_from_stack(stack_name=stack_name) 693 | self.instances = self.get_inst_from_asg(self.asg) 694 | 695 | try: 696 | if self.cfn_param_file_values['EnableEnaVfi']: 697 | print("Instances finishing booting") 698 | time.sleep(60) 699 | self.enable_ena_vfi(self.instances) 700 | except KeyError: 701 | pass 702 | 703 | try: 704 | if self.cfn_param_file_values['AddNetInterfaces']: 705 | self.add_net_dev() 706 | except KeyError: 707 | pass 708 | 709 | stk_output = self.get_stack_output(stack_name) 710 | 711 | try: 712 | eip = stk_output['ElasticIP'] 713 | self.set_elastic_ip(stack_eip=eip) 714 | except KeyError: 715 | pass 716 | 717 | self.stack_name = stack_name 718 | self.get_stack_info(stack_name=stack_name) 719 | 720 | return response 721 | 722 | def del_stack(self,stack_name, no_prompt=None): 723 | 724 | try: 725 | stk_response = self.client_cfn.describe_stacks(StackName=stack_name) 726 | 727 | if stk_response['Stacks'][0]['StackStatus'] == "DELETE_IN_PROGRESS": 728 | print('{0} already being deleted'.format(stack_name)) 729 | return 730 | 731 | for t in (stk_response['Stacks'][0]['Tags']): 732 | if t['Key'] == "cfnctl_param_file": 733 | f_path = os.path.join(self.cfn_param_file_dir, t['Value']) 734 | if os.path.isfile(f_path): 735 | 736 | if no_prompt: 737 | try: 738 | os.remove(f_path) 739 | print('Removed parameters file {0}'.format(f_path)) 740 | except Exception as e: 741 | raise ValueError(e) 742 | else: 743 | cli_val = input('Parameters file "{0}" exists, delete also? [y/N] '.format(f_path)) 744 | 745 | if not cli_val: 746 | cli_val = 'n' 747 | 748 | if cli_val.lower().startswith("y"): 749 | try: 750 | os.remove(f_path) 751 | print('Removed parameters file {0}'.format(f_path)) 752 | except Exception as e: 753 | raise ValueError(e) 754 | else: 755 | pass 756 | except ClientError as e: 757 | raise ValueError(e) 758 | 759 | print('Deleting {}'.format(stack_name)) 760 | try: 761 | response = self.client_cfn.delete_stack(StackName=stack_name) 762 | except Exception as e: 763 | raise ValueError(e) 764 | 765 | sc = response['ResponseMetadata']['HTTPStatusCode'] 766 | 767 | if sc != 200: 768 | errmsg = 'Problem deleting stack, status code {}'.format(sc) 769 | raise ValueError(errmsg) 770 | 771 | return 772 | 773 | def ls_stacks(self, stack_name=None, show_deleted=False): 774 | """ 775 | Using paginator for getting stack info, as the client.list_stack() will not get older stacks (>6 months) 776 | :param stack_name: stack_name 777 | :param show_deleted: Should we show deleted stacks also, StackStatus == DELETE_COMPLETE 778 | :return: dictionary of stacks, formatting needs to happen after the return 779 | 780 | """ 781 | 782 | all_stacks = list() 783 | 784 | paginator = self.client_cfn.get_paginator('list_stacks') 785 | response_iterator = paginator.paginate() 786 | 787 | stacks = dict() 788 | show_stack = False 789 | 790 | for page in response_iterator: 791 | all_stacks = page['StackSummaries'] 792 | for r in all_stacks: 793 | 794 | if [r['StackName']] == stack_name: 795 | show_stack = True 796 | elif show_deleted and r['StackStatus'] == "DELETE_COMPLETE": 797 | show_stack = True 798 | elif r['StackStatus'] == "DELETE_COMPLETE": 799 | show_stack = False 800 | else: 801 | show_stack = True 802 | 803 | if show_stack: 804 | try: 805 | stacks[r['StackName']] = [str(r['CreationTime']), r['StackStatus'], r['TemplateDescription']] 806 | except Exception as e: 807 | stacks[r['StackName']] = [str(r['CreationTime']), r['StackStatus'], "No Description"] 808 | 809 | return stacks 810 | 811 | def create_net_dev(self, subnet_id_n, desc, sg): 812 | """ 813 | Creates a network device, returns the id 814 | :return: network device id 815 | """ 816 | 817 | response = self.client_ec2.create_network_interface(SubnetId=subnet_id_n, Description=desc, Groups=[sg]) 818 | 819 | return response['NetworkInterface']['NetworkInterfaceId'] 820 | 821 | def attach_new_dev(self, i_id, dev_num, subnet_id, desc, sg): 822 | 823 | net_dev_to_attach = (self.create_net_dev(subnet_id, desc, sg)) 824 | 825 | response = self.client_ec2.attach_network_interface( 826 | DeviceIndex=dev_num, 827 | InstanceId=i_id, 828 | NetworkInterfaceId=net_dev_to_attach 829 | ) 830 | 831 | return response['AttachmentId'] 832 | 833 | def add_net_dev(self): 834 | 835 | print("Adding network interfaces") 836 | attach_resp = None 837 | 838 | for i in self.instances: 839 | 840 | instance = self.ec2.Instance(i) 841 | 842 | num_interfaces_b = (len(instance.network_interfaces)) 843 | num_interfaces = num_interfaces_b 844 | 845 | num_int_count = 0 846 | 847 | while num_interfaces < int(self.cfn_param_file_values['TotalNetInterfaces']): 848 | attach_resp = self.attach_new_dev(i, 849 | num_interfaces_b + num_int_count, 850 | self.cfn_param_file_values['Subnet'], 851 | self.stack_name + "-net_dev", 852 | self.cfn_param_file_values['SecurityGroups'] 853 | ) 854 | 855 | instance = self.ec2.Instance(i) 856 | 857 | num_interfaces = (len(instance.network_interfaces)) 858 | num_int_count += 1 859 | 860 | print(" {0} {1} {2}".format(instance.id, num_interfaces_b, num_interfaces)) 861 | time.sleep(10) 862 | 863 | return attach_resp 864 | 865 | def get_stack_events(self, stack_name): 866 | 867 | try: 868 | paginator = self.client_cfn.get_paginator('describe_stack_events') 869 | pages = paginator.paginate(StackName=stack_name, PaginationConfig={'MaxItems': 100}) 870 | return next(iter(pages))["StackEvents"] 871 | except Exception as e: 872 | raise ValueError(e) 873 | 874 | def stack_status(self, stack_name=None): 875 | 876 | if stack_name is None: 877 | stack_name = self.stack_name 878 | 879 | all_events = list() 880 | events = True 881 | 882 | stack_return_list = [ 883 | 'CREATE_COMPLETE', 884 | 'ROLLBACK_COMPLETE', 885 | 'CREATE_FAILED' 886 | ] 887 | 888 | while events: 889 | stk_status = self.get_stack_events(stack_name) 890 | 891 | for s in reversed(stk_status): 892 | event_id = s['EventId'] 893 | if event_id not in all_events: 894 | all_events.append(event_id) 895 | try: 896 | print('{0:<38} : {1:<25} : {2}'.format(s['LogicalResourceId'], s['ResourceStatus'], s['ResourceStatusReason'])) 897 | except KeyError: 898 | print('{0:<38} : {1:<25}'.format(s['LogicalResourceId'], s['ResourceStatus'])) 899 | except Exception as e: 900 | raise ValueError(e) 901 | 902 | if s['LogicalResourceId'] == stack_name and s['ResourceStatus'] in stack_return_list: 903 | events = False 904 | return s['ResourceStatus'] 905 | time.sleep(1) 906 | 907 | def has_elastic_ip(self, inst_arg=None): 908 | 909 | if not self.instances and inst_arg is None: 910 | print("Instance list is null, exiting") 911 | return 912 | 913 | if inst_arg is not None: 914 | self.instances = inst_arg 915 | 916 | for i in self.instances: 917 | response = self.client_ec2.describe_instances(InstanceIds=[i], DryRun=False) 918 | for r in response['Reservations']: 919 | for s in (r['Instances']): 920 | for interface in s['NetworkInterfaces']: 921 | response = self.client_ec2.describe_network_interfaces( 922 | NetworkInterfaceIds=[interface['NetworkInterfaceId']], 923 | DryRun=False) 924 | 925 | for r_net in response['NetworkInterfaces']: 926 | try: 927 | if r_net['Association'].get('AllocationId'): 928 | return r_net['Association'].get('PublicIp') 929 | except KeyError: 930 | pass 931 | 932 | def get_netdev0_id(self, instance=None): 933 | 934 | if instance is None: 935 | print("Must specify one instance") 936 | return 937 | 938 | response = self.client_ec2.describe_instances(InstanceIds=[instance], DryRun=False) 939 | for r in response['Reservations']: 940 | for s in (r['Instances']): 941 | for interface in s['NetworkInterfaces']: 942 | if interface['Attachment']['DeviceIndex'] == 0: 943 | return interface['NetworkInterfaceId'] 944 | 945 | def set_elastic_ip(self, instances=None, stack_eip=None): 946 | 947 | launch_time = dict() 948 | 949 | if instances is None: 950 | instances = self.instances 951 | 952 | has_eip = self.has_elastic_ip(instances) 953 | if has_eip: 954 | print('Elastic IP already allocated: ' + has_eip) 955 | return has_eip 956 | else: 957 | response = self.client_ec2.describe_instances(InstanceIds=instances, DryRun=False) 958 | for r in response['Reservations']: 959 | for resp_i in (r['Instances']): 960 | i = resp_i['InstanceId'] 961 | time_tuple = (resp_i['LaunchTime'].timetuple()) 962 | launch_time_secs = time.mktime(time_tuple) 963 | launch_time[i] = launch_time_secs 964 | 965 | launch_time_list = sorted(launch_time.items(), key=operator.itemgetter(1)) 966 | inst_to_alloc_eip = launch_time_list[1][0] 967 | 968 | netdev0 = self.get_netdev0_id(inst_to_alloc_eip) 969 | 970 | if not netdev0: 971 | print("Couldn't get first device") 972 | return 973 | 974 | try: 975 | if stack_eip is not None: 976 | allocation_id = self.get_net_alloc_id(stack_eip) 977 | ip_addr = stack_eip 978 | else: 979 | allocation = self.client_ec2.allocate_address(Domain='vpc') 980 | allocation_id = allocation['AllocationId'] 981 | ip_addr = allocation['PublicIp'] 982 | 983 | response = self.client_ec2.associate_address( 984 | AllocationId=allocation_id, 985 | NetworkInterfaceId=netdev0 986 | ) 987 | 988 | print('{0} now has Elastic IP address {1}'.format(inst_to_alloc_eip, ip_addr)) 989 | return ip_addr 990 | 991 | except ClientError as e: 992 | print(e) 993 | 994 | return response 995 | 996 | def get_stack_output(self, stack_name=None): 997 | 998 | if stack_name is None: 999 | stack_name = self.stack_name 1000 | 1001 | stk_response = None 1002 | 1003 | try: 1004 | stk_response = self.client_cfn.describe_stacks(StackName=stack_name) 1005 | except ClientError as e: 1006 | print(e) 1007 | 1008 | stk_output = dict() 1009 | 1010 | #for i in stk_response['Stacks']: 1011 | # try: 1012 | # for r in i['Outputs']: 1013 | # stk_output[r['OutputKey']] = r['OutputValue'] 1014 | # except KeyError: 1015 | # print("No Outputs found") 1016 | 1017 | return stk_output 1018 | 1019 | def get_net_alloc_id(self, ip=None): 1020 | 1021 | if ip is None: 1022 | print("Must specify an IP address") 1023 | return 1024 | 1025 | response = self.client_ec2.describe_addresses(PublicIps=[ip], DryRun=False) 1026 | for r in response['Addresses']: 1027 | return r['AllocationId'] 1028 | 1029 | def get_stack_info(self, stack_name=None): 1030 | 1031 | if stack_name is None: 1032 | stack_name = self.stack_name 1033 | 1034 | stack_status = self.ls_stacks(stack_name=stack_name) 1035 | for stack, i in sorted(stack_status.items()): 1036 | if stack == stack_name: 1037 | print("\nStatus:") 1038 | print('{0:<40.38} {1:<21.19} {2:<30.28} {3:<.30}'.format(stack, str(i[0]), i[1], i[2])) 1039 | print("") 1040 | 1041 | response = self.client_cfn.describe_stacks(StackName=stack_name) 1042 | 1043 | for i in response['Stacks']: 1044 | 1045 | print('[Parameters]') 1046 | try: 1047 | for p in i['Parameters']: 1048 | print('{0:<38} = {1:<30}'.format(p['ParameterKey'], p['ParameterValue'])) 1049 | except Exception as e: 1050 | print("No Parameters found") 1051 | pass 1052 | 1053 | print("") 1054 | 1055 | print('[Outputs]') 1056 | try: 1057 | for o in i['Outputs']: 1058 | print('{0:<38} = {1:<30}'.format(o['OutputKey'], o['OutputValue'])) 1059 | except Exception as e: 1060 | print("No Outputs found") 1061 | 1062 | print("") 1063 | return 1064 | 1065 | @staticmethod 1066 | def get_bucket_and_key_from_url(url): 1067 | 1068 | path = urlparse.urlparse(url).path 1069 | 1070 | path_l = path.split('/') 1071 | 1072 | bucket = path_l[1] 1073 | key = '/'.join(path_l[2:]) 1074 | 1075 | return bucket, key 1076 | 1077 | def get_cfn_param_file(self, template=None): 1078 | 1079 | self.cfn_param_file_basename = os.path.basename(template) 1080 | self.cfn_param_file = os.path.join(self.cfn_param_file_dir, self.cfn_param_file_basename) 1081 | 1082 | return self.cfn_param_file 1083 | 1084 | def rm_cfn_param_file(self, cfn_param_file=None): 1085 | 1086 | if cfn_param_file is None: 1087 | cfn_param_file = self.cfn_param_file 1088 | 1089 | print('Removing incomplete parameters file {0}'.format(cfn_param_file)) 1090 | 1091 | if os.path.exists(cfn_param_file): 1092 | os.remove(cfn_param_file) 1093 | return 1094 | else: 1095 | print('File does not exists: {0}'.format(cfn_param_file)) 1096 | sys.exit() 1097 | 1098 | sys.exit(1) 1099 | 1100 | def set_vpc_cfn_param_file(self, cfn_param_file='NULL', json_content=None, p=None ): 1101 | 1102 | ##print(self.vpc_variable_name) 1103 | 1104 | if cfn_param_file == 'NULL': 1105 | cfn_param_file = self.cfn_param_file 1106 | 1107 | print('Getting VPC info...') 1108 | 1109 | all_vpcs = self.get_vpcs() 1110 | 1111 | vpc_ids = list() 1112 | for vpc_k, vpc_values in all_vpcs.items(): 1113 | vpc_ids.append(vpc_k) 1114 | 1115 | #print(vpc_ids) 1116 | 1117 | for vpc_id, vpc_info in all_vpcs.items(): 1118 | try: 1119 | print(' {0} | {1} | {2} | {3}'.format(vpc_id, vpc_info['CidrBlock'], vpc_info['IsDefault'], 1120 | vpc_info['Tag_Name'])) 1121 | except: 1122 | print(' {0} | {1} | {2}'.format(vpc_id, vpc_info['CidrBlock'], vpc_info['IsDefault'])) 1123 | 1124 | prompt_msg = "Select VPC" 1125 | cli_val = self.get_cli_value(json_content, self.vpc_variable_name, prompt_msg) 1126 | 1127 | if cli_val not in vpc_ids: 1128 | print("Valid VPC required. Exiting... ") 1129 | self.rm_cfn_param_file(cfn_param_file) 1130 | return 1131 | 1132 | self.vpc_id = cli_val 1133 | 1134 | return self.vpc_id 1135 | 1136 | 1137 | def get_cli_value(self, json_content, p, prompt_msg, param_key=None): 1138 | 1139 | cli_val = "" 1140 | default_val = "" 1141 | 1142 | # Use region defaults file first 1143 | if os.path.exists(self.region_defaults): 1144 | self.read_cfn_param_file(self.region_defaults) 1145 | 1146 | if json_content['Parameters'][p]['Type'] == 'AWS::EC2::VPC::Id': 1147 | param_key = p 1148 | 1149 | 1150 | # Look for "Default" values in the CF template 1151 | try: 1152 | default_val = json_content['Parameters'][p]['Default'] 1153 | except KeyError: 1154 | pass 1155 | 1156 | # Override the default value in the template 1157 | # Look for values in the parameter file 1158 | try: 1159 | default_val = self.cfn_param_file_values[param_key] 1160 | except KeyError: 1161 | pass 1162 | 1163 | cli_val = input('{0} ({1}) [{2}]: '.format(prompt_msg, param_key, default_val)) 1164 | 1165 | if cli_val == "": 1166 | cli_val = default_val 1167 | 1168 | cli_val = cli_val.strip() 1169 | return cli_val 1170 | 1171 | 1172 | def build_cfn_param(self, stack_name, template, cli_template=None, verbose=False): 1173 | 1174 | command_line_template = cli_template 1175 | template_url = None 1176 | template_body = None 1177 | 1178 | value_already_set = list() 1179 | cfn_param_file_to_write = dict() 1180 | cfn_param_file = self.get_cfn_param_file(template + "." + stack_name) 1181 | cfn_param_file_default = self.get_cfn_param_file(template + ".default") 1182 | found_required_val = False 1183 | 1184 | if os.path.isfile(cfn_param_file_default): 1185 | cli_val = input("Default parameters file {0} exists, use this file [Y/n]: ".format(cfn_param_file_default)) 1186 | 1187 | if not cli_val: 1188 | cli_val = 'y' 1189 | 1190 | if cli_val.lower().startswith("n"): 1191 | try: 1192 | if os.path.isfile(cfn_param_file): 1193 | if not os.path.isfile(cfn_param_file_default): 1194 | cli_val = input("Parameters (not default) file {0} already exists, use this file [y/N]: ".format(cfn_param_file)) 1195 | 1196 | if not cli_val: 1197 | cli_val = 'n' 1198 | 1199 | if cli_val.lower().startswith("n"): 1200 | try: 1201 | os.remove(cfn_param_file) 1202 | self.cfn_param_file = cfn_param_file 1203 | except Exception as e: 1204 | raise ValueError(e) 1205 | else: 1206 | # params file already built, nothing left to do here 1207 | return cfn_param_file 1208 | else: 1209 | print('Stack parameters file does not exists, continuing...') 1210 | self.cfn_param_file = cfn_param_file 1211 | except Exception as e: 1212 | raise(ValueError(e)) 1213 | else: 1214 | # Using the already build .default params file, nothing left to do here 1215 | return cfn_param_file_default 1216 | 1217 | self.cfn_param_file = cfn_param_file 1218 | 1219 | if self.url_check(template): 1220 | template_url = template 1221 | 1222 | (bucket, key) = self.get_bucket_and_key_from_url(template_url) 1223 | s3_object = self.s3.Object(bucket, key) 1224 | try: 1225 | template_content = s3_object.get()['Body'].read().decode('utf-8') 1226 | except ClientError as e: 1227 | if e.response['Error']['Code'] == 'AccessDenied': 1228 | errmsg = "\nAccess Denied: Are you using the correct CFN template and region for the CFN template?" 1229 | raise ValueError(e[0] + errmsg) 1230 | elif e.response['Error']['Code'] == 'NoSuchKey': 1231 | errmsg = "\nCan't find {0} in bucket {1}".format(key, bucket) 1232 | raise ValueError(e[0] + errmsg) 1233 | raise ValueError(e) 1234 | else: 1235 | template_path = os.path.abspath(template) 1236 | template_body = template_path 1237 | template_content = self.parse_cfn_template(template) 1238 | 1239 | json_content = "" 1240 | try: 1241 | json_content = json.loads(template_content) 1242 | except json.decoder.JSONDecodeError: 1243 | try: 1244 | # this converts yaml to json using cfn_flip.to_json 1245 | json_content = json.loads(to_json(template_content)) 1246 | except Exception as e: 1247 | print(e) 1248 | print("Couldn't convert file {0} to JSON format".format(command_line_template)) 1249 | print(" -> The template has be in either JSON or YAML format") 1250 | sys.exit() 1251 | 1252 | if json_content == "": 1253 | print("Template file is blank, exiting...") 1254 | sys.exit() 1255 | 1256 | try: 1257 | if (json_content['Parameters']): 1258 | #for k in (json_content['Parameters']): 1259 | # print('key: ', k) 1260 | pass 1261 | except KeyError: 1262 | message = 'The CloudFormation template does not have any parameters' 1263 | print(message) 1264 | return "NO_PARAM_FILE" 1265 | sys.exit() 1266 | 1267 | # create parameters file and dir 1268 | if not os.path.isfile(cfn_param_file): 1269 | print("Creating parameters file {0}".format(cfn_param_file)) 1270 | if not os.path.isdir(self.cfn_param_file_dir): 1271 | print("Creating parameters directory {0}".format(self.cfn_param_file_dir)) 1272 | try: 1273 | os.makedirs(self.cfn_param_file_dir) 1274 | except OSError as e: 1275 | if e.errno != errno.EEXIST: 1276 | raise 1277 | 1278 | elif os.path.isfile(cfn_param_file): 1279 | cli_val = input("Parameters file {0} already exists, use this file [y/N]: ".format(cfn_param_file)) 1280 | 1281 | if not cli_val: 1282 | cli_val = 'n' 1283 | 1284 | if cli_val.lower().startswith("n"): 1285 | try: 1286 | os.remove(cfn_param_file) 1287 | self.cfn_param_file = cfn_param_file 1288 | except Exception as e: 1289 | raise ValueError(e) 1290 | else: 1291 | # params file already build, nothing left to do here 1292 | return cfn_param_file 1293 | 1294 | for p in sorted(json_content['Parameters']): 1295 | if json_content['Parameters'][p]['Type'] == 'AWS::EC2::VPC::Id': 1296 | self.vpc_variable_name=p 1297 | 1298 | 1299 | for p in sorted(json_content['Parameters']): 1300 | param_key = p 1301 | 1302 | default_val = None 1303 | cli_val = None 1304 | param_type = None 1305 | 1306 | # Debug 1307 | #print('setting {0}'.format(p)) 1308 | 1309 | # get and set AWS::EC2::KeyPair::KeyName 1310 | try: 1311 | if json_content['Parameters'][p]['Type'] == 'AWS::EC2::KeyPair::KeyName': 1312 | if not self.key_pairs: 1313 | print('No EC2 keys found in {0}'.format(self.region)) 1314 | self.rm_cfn_param_file(cfn_param_file) 1315 | return 1316 | print('EC2 keys found in {0}:'.format(self.region)) 1317 | #print(' {0}'.format(', '.join(self.key_pairs))) 1318 | for k in self.key_pairs: 1319 | print(' {0}'.format(k)) 1320 | 1321 | prompt_msg = "Select EC2 Key" 1322 | cli_val = self.get_cli_value(json_content, p, prompt_msg, param_key) 1323 | 1324 | if cli_val not in self.key_pairs: 1325 | print("Valid EC2 Key Pair required. Exiting... ") 1326 | self.rm_cfn_param_file(cfn_param_file) 1327 | sys.exit() 1328 | except Exception as e: 1329 | print(e) 1330 | 1331 | # get and set AWS::EC2::VPC::Id 1332 | 1333 | try: 1334 | if json_content['Parameters'][p]['Type'] == 'AWS::EC2::VPC::Id': 1335 | if self.vpc_id is None: 1336 | self.vpc_id = self.set_vpc_cfn_param_file(cfn_param_file,json_content=json_content,p=p) 1337 | cfn_param_file_to_write[p] = self.vpc_id 1338 | value_already_set.append(p) 1339 | else: 1340 | cfn_param_file_to_write[p] = self.vpc_id 1341 | value_already_set.append(p) 1342 | 1343 | except Exception as e: 1344 | print(e) 1345 | 1346 | # get and set List 1347 | try: 1348 | if json_content['Parameters'][p]['Type'] == 'List' or \ 1349 | json_content['Parameters'][p]['Type'] == 'AWS::EC2::Subnet::Id': 1350 | 1351 | if self.vpc_id is None: 1352 | try: 1353 | self.vpc_id = self.set_vpc_cfn_param_file(cfn_param_file,json_content=json_content,p=p) 1354 | except Exception as e: 1355 | raise ValueError(e) 1356 | 1357 | print('Getting subnets for {0} ...'.format(self.vpc_id)) 1358 | 1359 | subnet_ids = list() 1360 | all_subnets = self.get_subnets_from_vpc(self.vpc_id) 1361 | for subnet_id, subnet_info in all_subnets.items(): 1362 | subnet_ids.append(subnet_id) 1363 | try: 1364 | print(' {0} | {1} | {2}'.format(subnet_id, subnet_info['AvailabilityZone'], 1365 | subnet_info['Tag_Name'][0:20])) 1366 | except KeyError: 1367 | print(' {0} | {1}'.format(subnet_id, subnet_info['AvailabilityZone'])) 1368 | 1369 | prompt_msg = "Select subnet" 1370 | cli_val = self.get_cli_value(json_content, p, prompt_msg, param_key) 1371 | 1372 | if cli_val not in subnet_ids: 1373 | print("Valid subnet ID required. Exiting... ") 1374 | self.rm_cfn_param_file(cfn_param_file) 1375 | return 1376 | except Exception as e: 1377 | pass 1378 | 1379 | # get and set AWS::EC2::SecurityGroup::Id 1380 | try: 1381 | if json_content['Parameters'][p]['Type'] == 'AWS::EC2::SecurityGroup::Id': 1382 | 1383 | if self.vpc_id is None: 1384 | try: 1385 | self.vpc_id = self.set_vpc_cfn_param_file(cfn_param_file,json_content=json_content,p=p) 1386 | except Exception as e: 1387 | raise ValueError(e) 1388 | 1389 | print('Getting security groups for {0} ...'.format(self.vpc_id)) 1390 | 1391 | security_group_ids = list() 1392 | all_security_group_info = self.get_security_groups(self.vpc_id) 1393 | 1394 | for r in all_security_group_info: 1395 | security_group_ids.append(r['GroupId']) 1396 | print(' {0} | {1}'.format(r['GroupId'], r['GroupName'][0:20])) 1397 | prompt_msg = "Select secuirty group" 1398 | cli_val = self.get_cli_value(json_content, p, prompt_msg, param_key) 1399 | if cli_val not in security_group_ids: 1400 | print("Valid security group required. Exiting... ") 1401 | self.rm_cfn_param_file(cfn_param_file) 1402 | except Exception as e: 1403 | print(e) 1404 | 1405 | try: 1406 | default_val = json_content['Parameters'][p]['Default'] 1407 | except KeyError: 1408 | pass 1409 | 1410 | try: 1411 | param_type = json_content['Parameters'][p]['Type'] 1412 | except KeyError: 1413 | pass 1414 | 1415 | 1416 | if cli_val is None and p not in value_already_set: 1417 | try: 1418 | if default_val is None: 1419 | default_val = "" 1420 | 1421 | if verbose: 1422 | print 1423 | print('# Parameter: {}'.format(p)) 1424 | 1425 | try: 1426 | print('# Description: {0}'.format(json_content['Parameters'][p]['Description'])) 1427 | except KeyError: 1428 | pass 1429 | 1430 | try: 1431 | print('# Type: {0}'.format(json_content['Parameters'][p]['Type'])) 1432 | except KeyError: 1433 | pass 1434 | 1435 | print('# Default: {0}'.format(default_val)) 1436 | 1437 | try: 1438 | print('# ConstraintDescription: {0}'.format(json_content['Parameters'][p]['ConstraintDescription'])) 1439 | except KeyError: 1440 | pass 1441 | 1442 | try: 1443 | space = 16 * " " 1444 | a_val = ' '.join((json_content['Parameters'][p]['AllowedValues'])) 1445 | a_val_formatted = textwrap.wrap(a_val, width=80, replace_whitespace=False) 1446 | a_val_formatted_0 = a_val_formatted[0] 1447 | a_val_formatted_1 = '\n#{0}'.join(a_val_formatted[1:]).format(space) 1448 | 1449 | print('# AllowedValues: {0}\n#{1}{2}'.format(a_val_formatted_0, space, a_val_formatted_1)) 1450 | 1451 | except KeyError: 1452 | pass 1453 | 1454 | ## Grab any defaults from the region default file 1455 | if os.path.exists(self.region_defaults): 1456 | self.read_cfn_param_file(self.region_defaults) 1457 | default_val = self.cfn_param_file_values[p] 1458 | 1459 | cli_val = input('Select {0} [{1}]: '.format(p, default_val)) 1460 | 1461 | if cli_val == "": 1462 | cli_val = default_val 1463 | 1464 | except Exception as e: 1465 | print(e) 1466 | 1467 | try: 1468 | if cli_val == "" and default_val == "" and json_content['Parameters'][p]['ConstraintDescription']: 1469 | if self.cfn_action == "build": 1470 | print(' WARNING ONLY: Parameter "{0}" is required but can be updated in ' 1471 | 'parameters file and left empty for now'.format(p)) 1472 | elif self.cfn_action == "create": 1473 | print(' REQUIREMENT: Parameter "{0}" is required to create the stack, please enter a value,' 1474 | ' optionally exit create and rerun with build action'.format(p)) 1475 | 1476 | cli_val = input('{0}: '.format(p)) 1477 | if cli_val == "": 1478 | if self.cfn_action == "build": 1479 | cli_val = "" 1480 | found_required_val = True 1481 | else: 1482 | print("Required parameter {0} not entered. The stack create will fail, but the " 1483 | "parameters file will still be built.".format(p)) 1484 | except: 1485 | pass 1486 | 1487 | try: 1488 | if p not in value_already_set: 1489 | if cli_val is not None: 1490 | cfn_param_file_to_write[p] = cli_val 1491 | value_already_set.append(p) 1492 | elif default_val is not None: 1493 | cfn_param_file_to_write[p] = default_val 1494 | value_already_set.append(p) 1495 | else: 1496 | cfn_param_file_to_write[p] = "" 1497 | value_already_set.append(p) 1498 | except KeyError: 1499 | pass 1500 | 1501 | if found_required_val: 1502 | print('Some values are still needed, replace "" in {0}'.format(cfn_param_file)) 1503 | 1504 | # Debug 1505 | # print (sorted(cfn_param_file_to_write.items())) 1506 | with open(self.cfn_param_file, 'w') as cfn_out_file: 1507 | 1508 | cfn_out_file.write('[AWS-Config]\n') 1509 | if template_url is not None: 1510 | cfn_out_file.write('{0} = {1}\n'.format('TemplateURL', template_url)) 1511 | elif template_body is not None: 1512 | cfn_out_file.write('{0} = {1}\n'.format('TemplateBody', template_body)) 1513 | cfn_out_file.write('\n') 1514 | 1515 | cfn_out_file.write('[Paramters]\n') 1516 | 1517 | for k, v in sorted(cfn_param_file_to_write.items()): 1518 | cfn_out_file.write('{0:<35} = {1}\n'.format(k, v)) 1519 | 1520 | print("Done building cfnctl parameters file {0}, includes template location".format(cfn_param_file)) 1521 | 1522 | return cfn_param_file 1523 | 1524 | def get_instance_info(self, instance_state=None): 1525 | 1526 | # returns a dictionary 1527 | 1528 | # Instance state can be: pending | running | shutting-down | terminated | stopping | stopped 1529 | 1530 | instance_states = ['pending', 1531 | 'running', 1532 | 'shutting-down', 1533 | 'terminated', 1534 | 'stopping', 1535 | 'stopped' 1536 | ] 1537 | 1538 | if instance_state is not None and instance_state not in instance_states: 1539 | errmsg = 'Instance state "{0}" not valid. ' \ 1540 | 'Choose "pending | running | shutting-down | terminated | stopping | stopped"'.format(instance_state) 1541 | raise ValueError(errmsg) 1542 | 1543 | if instance_state is None: 1544 | instances = self.ec2.instances.all() 1545 | else: 1546 | instances = self.ec2.instances.filter(Filters=[{ 1547 | 'Name': 'instance-state-name', 1548 | 'Values': [instance_state]}] 1549 | ) 1550 | 1551 | inst_info = dict() 1552 | for i in instances: 1553 | tag_name = 'NULL' 1554 | try: 1555 | for tag in i.tags: 1556 | if tag['Key'] == 'Name': 1557 | tag_name = tag['Value'] 1558 | except: 1559 | pass 1560 | inst_info[i.id] = { 1561 | 'TAG::Name': tag_name, 1562 | 'Type': i.instance_type, 1563 | 'State': i.state['Name'], 1564 | 'Private IP': i.private_ip_address, 1565 | 'Private DNS': i.private_dns_name, 1566 | 'Public IP': i.public_ip_address, 1567 | 'Launch Time': i.launch_time 1568 | } 1569 | 1570 | return inst_info # returns a dictionary 1571 | 1572 | def get_vpcs(self): 1573 | 1574 | response = self.client_ec2.describe_vpcs() 1575 | 1576 | vpc_keys_all = [ 1577 | 'Tag_Name', 1578 | 'VpcId', 1579 | 'InstanceTenancy', 1580 | 'Tags', 1581 | 'State', 1582 | 'DhcpOptionsId', 1583 | 'CidrBlock', 1584 | 'IsDefault' 1585 | ] 1586 | 1587 | all_vpcs = dict() 1588 | 1589 | for v in response['Vpcs']: 1590 | all_vpcs[v['VpcId']] = dict() 1591 | try: 1592 | for t in (v['Tags']): 1593 | if (t['Key']) == 'Name': 1594 | all_vpcs[v['VpcId']]['Tag_Name'] = t['Value'] 1595 | except KeyError: 1596 | pass 1597 | 1598 | for vpc_key in self.vpc_keys_to_print: 1599 | try: 1600 | all_vpcs[v['VpcId']][vpc_key] = v[vpc_key] 1601 | except KeyError: 1602 | pass 1603 | 1604 | return all_vpcs 1605 | 1606 | def get_subnets_from_vpc(self, vpc_to_get): 1607 | 1608 | subnet_keys_all = ['Tag_Name', 1609 | 'VpcId', 1610 | 'Tags', 1611 | 'AvailableIpAddressCount', 1612 | 'MapPublicIpOnLaunch', 1613 | 'DefaultForAz', 1614 | 'Ipv6CidrBlockAssociationSet', 1615 | 'State', 1616 | 'AvailabilityZone', 1617 | 'SubnetId', 1618 | 'CidrBlock', 1619 | 'AssignIpv6AddressOnCreation' 1620 | ] 1621 | 1622 | response = self.client_ec2.describe_subnets(Filters=[{'Name': 'vpc-id', 'Values': [vpc_to_get]}]) 1623 | 1624 | all_subnets = dict() 1625 | 1626 | for r in response['Subnets']: 1627 | all_subnets[r['SubnetId']] = dict() 1628 | try: 1629 | for t in (r['Tags']): 1630 | if (t['Key']) == 'Name': 1631 | all_subnets[r['SubnetId']]['Tag_Name'] = t['Value'] 1632 | except KeyError: 1633 | pass 1634 | 1635 | for sn_key in subnet_keys_all: 1636 | try: 1637 | all_subnets[r['SubnetId']][sn_key] = r[sn_key] 1638 | except KeyError: 1639 | pass 1640 | 1641 | return all_subnets 1642 | 1643 | def get_security_groups(self, vpc=None): 1644 | 1645 | sec_groups_all_keys = [ 1646 | 'IpPermissionsEgress', 1647 | 'Description', 1648 | 'GroupName', 1649 | 'VpcId', 1650 | 'OwnerId', 1651 | 'GroupId', 1652 | ] 1653 | 1654 | if vpc is None: 1655 | try: 1656 | response = self.client_ec2.describe_security_groups() 1657 | except Exception as e: 1658 | raise ValueError(e) 1659 | else: 1660 | try: 1661 | response = self.client_ec2.describe_security_groups( Filters=[{'Name': 'vpc-id', 'Values': [vpc]}] ) 1662 | except Exception as e: 1663 | raise ValueError(e) 1664 | 1665 | return response['SecurityGroups'] 1666 | 1667 | def validate_cfn_template(self, template_url=None, template_body=None): 1668 | 1669 | response = None 1670 | 1671 | if template_url is not None and template_body is not None: 1672 | errmsg = "Specify either TemplateURL or TemplateBody, not both" 1673 | raise ValueError(errmsg) 1674 | 1675 | if template_url is not None: 1676 | try: 1677 | response = self.client_cfn.validate_template(TemplateURL=template_url) 1678 | except Exception as e: 1679 | raise ValueError("validate_cfn_template: " + e[0]) 1680 | elif template_body is not None: 1681 | try: 1682 | template_body = self.parse_cfn_template(template_body) 1683 | response = self.client_cfn.validate_template(TemplateBody=template_body) 1684 | except Exception as e: 1685 | errmsg = e[0] 1686 | if "Member must have length less than or equal to 51200" in e[0]: 1687 | errmsg = " Member must have length less than or equal to 51200" 1688 | raise ValueError(errmsg) 1689 | 1690 | return 1691 | 1692 | def parse_cfn_template(self, template=None): 1693 | 1694 | if template is None: 1695 | template = self.template 1696 | 1697 | with open(template) as file: 1698 | template_obj = file.read() 1699 | 1700 | return template_obj 1701 | 1702 | def upload_to_bucket(self, filename, bucket, key): 1703 | """ 1704 | 1705 | :param bucket: bucket name 1706 | :param filename: file to upload 1707 | :return: bucket URL 1708 | """ 1709 | key = filename 1710 | filename_path = os.path.abspath(filename) 1711 | 1712 | try: 1713 | response = self.client_s3.upload_file(filename_path, bucket, key) 1714 | except Exception as e: 1715 | raise ValueError(e) 1716 | 1717 | url = '{}/{}/{}'.format(self.client_s3.meta.endpoint_url, bucket, key) 1718 | 1719 | return url 1720 | 1721 | def setup(self): 1722 | pass 1723 | 1724 | def teardown(self): 1725 | pass 1726 | -------------------------------------------------------------------------------- /aws-cfn-control/awscfnctl/build_ami_maps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 7 | # except in compliance with the License. A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 12 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under the License. 14 | # 15 | 16 | import sys 17 | import json 18 | import boto3 19 | import argparse 20 | 21 | #aws ec2 describe-images --owners 309956199498 --region us-west-2 --filters Name=name,Values=RHEL-7.3_HVM_GA-20161026-x86_64-1-Hourly2-GP2 22 | 23 | def arg_parse(): 24 | parser = argparse.ArgumentParser(prog='get_ami_id') 25 | parser.add_argument('--amzn', 26 | dest='alinux', 27 | type=str, 28 | help='Base Amazon Linux AMI ID (xx-xxxxxxxxx) *specifically* in us-east-1, ' 29 | 'use this site for Amazon Linux: ' 30 | ' https://aws.amazon.com/amazon-linux-ami/', 31 | required=False 32 | ) 33 | parser.add_argument('--amzn2', 34 | dest='alinux2', 35 | type=str, 36 | help='Base Amazon Linux 2 AMI ID (xx-xxxxxxxxx) *specifically* in us-east-1, ' 37 | 'use this site for Amazon Linux 2: ' 38 | ' https://aws.amazon.com/amazon-linux-ami/', 39 | required=False 40 | ) 41 | parser.add_argument('--centos6', 42 | dest='centos6', 43 | type=str, 44 | help='Base CentOS6 Linux AMI ID (xx-xxxxxxxxx) *specifically* in us-east-1, ' 45 | 'use this site for CentOS AMI info: ' 46 | ' https://wiki.centos.org/Cloud/AWS', 47 | required=False 48 | ) 49 | parser.add_argument('--centos7', 50 | dest='centos7', 51 | type=str, 52 | help='Base Centos7 Linux AMI ID (xx-xxxxxxxxx) *specifically* in us-east-1, ' 53 | 'use this site for CentOS AMI info: ' 54 | ' https://wiki.centos.org/Cloud/AWS', 55 | required=False 56 | ) 57 | parser.add_argument('--rhel7', 58 | dest='rhel7', 59 | type=str, 60 | help='Base RHEL7 Linux AMI ID (xx-xxxxxxxxx) *specifically* in us-east-1, ' 61 | 'use this site for RHEL 7 AMI info' 62 | ' AWS Console', 63 | required=False 64 | ) 65 | parser.add_argument('--suse11', 66 | dest='suse11', 67 | type=str, 68 | help='Base SUSE 11 Linux AMI ID (xx-xxxxxxxxx) *specifically* in us-east-1, ' 69 | 'use this site for SuSE 11 info: ' 70 | ' AWS Console', 71 | required=False 72 | ) 73 | parser.add_argument('--suse12', 74 | dest='suse12', 75 | type=str, 76 | help='Base SUSE 12 Linux AMI ID (xx-xxxxxxxxx) *specifically* in us-east-1, ' 77 | 'use this site for SuSE 12 info: ' 78 | ' AWS Console', 79 | required=False 80 | ) 81 | parser.add_argument('--ubuntu14', 82 | dest='ubuntu14', 83 | type=str, 84 | help='Base Ubuntu 14 Linux AMI ID (xx-xxxxxxxxx) *specifically* in us-east-1, ' 85 | 'use this site for Ubuntu14: ' 86 | ' AWS Console', 87 | required=False 88 | ) 89 | parser.add_argument('--ubuntu16', 90 | dest='ubuntu16', 91 | type=str, 92 | help='Base Ubuntu 16 Linux AMI ID (xx-xxxxxxxxx) *specifically* in us-east-1, ' 93 | 'use this site for Ubuntu16: ' 94 | ' AWS Console', 95 | required=False 96 | ) 97 | 98 | return parser.parse_args() 99 | 100 | 101 | def image_info(client, owners, ami_name, region): 102 | 103 | response = client.describe_images( 104 | DryRun=False, 105 | Owners=[ 106 | owners, 107 | ], 108 | Filters=[ 109 | { 110 | 'Name': 'name', 111 | 'Values': [ 112 | ami_name, 113 | ] 114 | }, 115 | ] 116 | ) 117 | 118 | try: 119 | if response["Images"][0]["ImageId"]: 120 | return response 121 | except: 122 | print("Does the AMI requested exist in {0}? Not adding region {0} to list. Continuing...".format(region)) 123 | return "NONE" 124 | 125 | 126 | def get_image_info(client, ami_id): 127 | 128 | try: 129 | response = client.describe_images( 130 | DryRun=False, 131 | ImageIds=[ 132 | ami_id, 133 | ], 134 | ) 135 | except Exception as e: 136 | print(e) 137 | print("Does {0} exist in us-east-1? Checking next region ...".format(ami_id)) 138 | sys.exit(1) 139 | 140 | ami_name = response["Images"][0]["Name"] 141 | owners = 'NONE' 142 | description = 'NONE' 143 | ena = 'NONE' 144 | sriov = 'NONE' 145 | 146 | try: 147 | owners = response["Images"][0]["OwnerId"] 148 | description = response["Images"][0]["Description"] 149 | ena = response["Images"][0]["EnaSupport"] 150 | sriov = response["Images"][0]["SriovNetSupport"] 151 | except KeyError as e: 152 | pass 153 | 154 | return ami_name, owners, description, ena, sriov 155 | 156 | 157 | def print_image_info(args, client): 158 | 159 | for arg_n, ami_id in vars(args).items(): 160 | if ami_id: 161 | (ami_name, owners, description, ena, sriov) = get_image_info(client, ami_id) 162 | print('Building mappings for:\n' 163 | ' Argument Name: {0}\n' 164 | ' AMI Name: {1}\n' 165 | ' AMI ID: {2}\n' 166 | ' Owners ID: {3}\n' 167 | ' AMI Desc: {4}\n' 168 | ' ENA Support: {5}\n' 169 | ' SRIOV Support: {6}\n' 170 | .format(arg_n, ami_name, ami_id, owners, description, ena, sriov)) 171 | 172 | def main(): 173 | 174 | rc = 0 175 | 176 | ami_map = dict() 177 | 178 | args = arg_parse() 179 | 180 | client_iad = boto3.client('ec2', region_name='us-east-1') 181 | r_response_iad = client_iad.describe_regions() 182 | 183 | print_image_info(args, client_iad) 184 | print("Getting AMI IDs from regions: ") 185 | 186 | for r in r_response_iad["Regions"]: 187 | region=r["RegionName"] 188 | print(" " + region) 189 | 190 | client = boto3.client('ec2', region_name=region) 191 | 192 | response = dict() 193 | ami_map[region] = dict() 194 | 195 | for arg_n, ami_id_iad in vars(args).items(): 196 | if ami_id_iad: 197 | (ami_name, owners, description, ena, sriov) = get_image_info(client_iad, ami_id_iad) 198 | response[arg_n] = image_info(client, owners, ami_name, region) 199 | if response[arg_n] is not "NONE": 200 | ami_map[region].update({arg_n: response[arg_n]["Images"][0]["ImageId"]}) 201 | 202 | ami_map = { "AWSRegionAMI": ami_map } 203 | ami_map = { "Mappings": ami_map } 204 | 205 | print(json.dumps(ami_map, indent=2, sort_keys=True)) 206 | 207 | ##print(ami_map) 208 | 209 | return rc 210 | 211 | 212 | if __name__ == "__main__": 213 | try: 214 | sys.exit(main()) 215 | except KeyboardInterrupt: 216 | print('\nReceived Keyboard interrupt.') 217 | print('Exiting...') 218 | except ValueError as e: 219 | print('ERROR: {0}'.format(e)) 220 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /aws-cfn-control/awscfnctl/cfnctl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 7 | # except in compliance with the License. A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 12 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under the License. 14 | # 15 | 16 | import os 17 | import sys 18 | import argparse 19 | from awscfnctl import CfnControl 20 | from argparse import RawTextHelpFormatter 21 | 22 | progname = 'cfnctl' 23 | 24 | 25 | def arg_parse(): 26 | 27 | parser = argparse.ArgumentParser(prog=progname, 28 | description='Launch and manage CloudFormation templates from the command line', 29 | formatter_class=RawTextHelpFormatter 30 | ) 31 | parser._optionals.title = "arguments" 32 | 33 | parser.add_argument('cfn_action', type=str, 34 | help="REQUIRED: Action: build|create|list|delete\n" 35 | " build Builds the CFN parameter file (-t required)\n" 36 | " create Creates a new stack (-n and [-t|-f] required)\n" 37 | " list List all stacks (-d provides extra detail)\n" 38 | " delete Deletes a stack (-n is required)" 39 | ) 40 | parser.add_argument('-r', dest='region', required=False, help="Region name") 41 | parser.add_argument('-n', dest='stack_name', required=False, help="Stack name") 42 | parser.add_argument('-t', dest='template', required=False, help='CFN Template from local file or S3 URL') 43 | parser.add_argument('-f', dest='param_file', required=False, 44 | help="Template parameter file") 45 | parser.add_argument('-d', dest='ls_all_stack_info', required=False, help='List details on all stacks', 46 | action='store_true') 47 | parser.add_argument('-b', dest='bucket', required=False, help='Bucket to upload template to') 48 | parser.add_argument('-nr', dest='no_rollback', required=False, help='Do not rollback', action='store_true') 49 | parser.add_argument('-p', dest='aws_profile', required=False, help='AWS Profile') 50 | parser.add_argument('-y', dest='no_prompt', required=False, help='On interactive question, force yes', 51 | action='store_true') 52 | parser.add_argument('-v', dest='verbose_param_file', required=False, help='Verbose config file', 53 | action='store_true') 54 | 55 | if len(sys.argv[1:]) == 0: 56 | parser.print_help() 57 | parser.exit() 58 | 59 | return parser.parse_args() 60 | 61 | 62 | def main(): 63 | 64 | rc = 0 65 | args = arg_parse() 66 | rollback = 'ROLLBACK' 67 | 68 | create_stack = False 69 | del_stack = False 70 | ls_stacks = False 71 | build_param_file = False 72 | 73 | cfn_action = args.cfn_action 74 | if cfn_action == "create": 75 | create_stack = True 76 | elif cfn_action == "delete": 77 | del_stack = True 78 | elif cfn_action == "list": 79 | ls_stacks = True 80 | elif cfn_action == "build": 81 | build_param_file = True 82 | else: 83 | print('Action has to be "build|create|list|delete"') 84 | sys.exit(1) 85 | 86 | bucket = args.bucket 87 | param_file = args.param_file 88 | ls_all_stack_info = args.ls_all_stack_info 89 | region = args.region 90 | stack_name = args.stack_name 91 | template = args.template 92 | no_prompt = args.no_prompt 93 | verbose_param_file = args.verbose_param_file 94 | 95 | errmsg_cr = "Creating a stack requires 'create' action, stack name (-n), and for new stacks " \ 96 | "the template (-t) flag or for configured stacks, the -f flag for parameters file, " \ 97 | "which includes the template location" 98 | 99 | aws_profile = None 100 | if args.aws_profile: 101 | aws_profile = args.aws_profile 102 | # print('Using profile {0}'.format(aws_profile)) 103 | else: 104 | aws_profile = os.environ.get('AWS_DEFAULT_PROFILE', None) 105 | # print('Using environment variable profile {0}'.format(aws_profile)) 106 | 107 | if args.no_rollback: 108 | rollback = 'DO_NOTHING' 109 | 110 | client = CfnControl(region=region, aws_profile=aws_profile, cfn_action=cfn_action) 111 | 112 | if ls_stacks and stack_name: 113 | stacks = client.ls_stacks(show_deleted=False) 114 | for stack, i in sorted(stacks.items()): 115 | if stack_name == stack: 116 | client.get_stack_info(stack_name=stack_name) 117 | 118 | elif ls_all_stack_info or ls_stacks: 119 | if ls_all_stack_info and ls_stacks: 120 | print("Gathering all info on CFN stacks...") 121 | stacks = client.ls_stacks(show_deleted=False) 122 | for stack, i in sorted(stacks.items()): 123 | if len(stack) > 37: 124 | stack = stack[:37] + ">" 125 | print('{0:<42.40} {1:<21.19} {2:<30.28} {3:<.30}'.format(stack, str(i[0]), i[1], i[2])) 126 | elif ls_stacks: 127 | print("Listing stacks...") 128 | stacks = client.ls_stacks(show_deleted=False) 129 | for stack, i in sorted(stacks.items()): 130 | print(' {}'.format(stack)) 131 | elif create_stack: 132 | if stack_name and param_file and not template: 133 | response = "" 134 | try: 135 | response = client.cr_stack(stack_name, param_file, verbose=verbose_param_file, set_rollback=rollback) 136 | except Exception as cr_stack_err: 137 | print("Got response: {0}".format(response)) 138 | raise ValueError(cr_stack_err) 139 | 140 | elif template and stack_name: 141 | 142 | if not client.url_check(template): 143 | if not os.path.isfile(template): 144 | errmsg = 'File "{}" does not exists'.format(template) 145 | raise ValueError(errmsg) 146 | try: 147 | if param_file: 148 | param_file = param_file 149 | print("Parameters file specified at CLI: {}".format(param_file)) 150 | 151 | response = client.cr_stack(stack_name, param_file, verbose=verbose_param_file, set_rollback=rollback, 152 | template=template) 153 | #return response 154 | return 155 | 156 | except Exception as cr_stack_err: 157 | if "Member must have length less than or equal to 51200" in str(cr_stack_err): 158 | if bucket: 159 | print("Uploading {0} to bucket {1} and creating stack".format(template, bucket)) 160 | response = "" 161 | try: 162 | template_url = client.upload_to_bucket(template, bucket, template) 163 | response = client.cr_stack(stack_name, param_file, verbose=verbose_param_file, 164 | set_rollback=rollback, template=template_url) 165 | except Exception as upload_to_bucket_err: 166 | print("Got response: {0}".format(response)) 167 | raise ValueError(upload_to_bucket_err) 168 | else: 169 | errmsg = "The template has too many bytes (>51,200), use the -b flag with a bucket name, or " \ 170 | "upload the template to an s3 bucket and specify the bucket URL with the -t flag " 171 | raise ValueError(errmsg) 172 | else: 173 | raise ValueError(cr_stack_err) 174 | elif template and not stack_name: 175 | raise ValueError(errmsg_cr) 176 | elif not template and stack_name: 177 | raise ValueError(errmsg_cr) 178 | elif not template and not stack_name: 179 | raise ValueError(errmsg_cr) 180 | elif del_stack: 181 | if not stack_name: 182 | errmsg = "Must specify a stack to delete (-n)" 183 | raise ValueError(errmsg) 184 | client.del_stack(stack_name, no_prompt=no_prompt) 185 | elif build_param_file: 186 | client.build_cfn_param('default', template, cli_template=template) 187 | elif param_file or stack_name: 188 | raise ValueError(errmsg_cr) 189 | else: 190 | print("No actions requested - shouldn't have got this far.") 191 | return 0 192 | 193 | return rc 194 | 195 | 196 | if __name__ == "__main__": 197 | try: 198 | sys.exit(main()) 199 | except KeyboardInterrupt: 200 | print('\nReceived Keyboard interrupt.') 201 | print('Exiting...') 202 | except SystemExit: 203 | print('Exiting...') 204 | except ValueError as e: 205 | print('ERROR: {0}'.format(e)) 206 | except Exception as e: 207 | print('ERROR: {0}'.format(e)) 208 | -------------------------------------------------------------------------------- /aws-cfn-control/awscfnctl/get_asg_from_stack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 7 | # except in compliance with the License. A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 12 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under the License. 14 | # 15 | 16 | import sys 17 | import boto3 18 | import argparse 19 | from awscfnctl import CfnControl 20 | 21 | progname = 'get_asg_from_stack' 22 | 23 | def arg_parse(): 24 | 25 | parser = argparse.ArgumentParser(prog=progname, description='Launch a stack, with a config file') 26 | 27 | opt_group = parser.add_argument_group() 28 | opt_group.add_argument('-r', dest='region', required=False, help="Region name") 29 | 30 | req_group = parser.add_argument_group('required arguments') 31 | req_group.add_argument('-s', dest='stack_name', required=True) 32 | 33 | return parser.parse_args() 34 | 35 | 36 | def get_asg_from_stack(stack_name, client): 37 | 38 | asg = list() 39 | 40 | stk_response = client.describe_stack_resources(StackName=stack_name) 41 | 42 | for resp in stk_response['StackResources']: 43 | for resrc_type in resp: 44 | if resrc_type == "ResourceType": 45 | if resp[resrc_type] == "AWS::AutoScaling::AutoScalingGroup": 46 | asg.append(resp['PhysicalResourceId']) 47 | 48 | return asg 49 | 50 | 51 | def main(): 52 | 53 | rc = 0 54 | 55 | args = arg_parse() 56 | 57 | region = args.region 58 | stack = args.stack_name 59 | 60 | cfn_client = boto3.client('cloudformation', region_name=region) 61 | 62 | asg = get_asg_from_stack(stack, cfn_client) 63 | 64 | for a in asg: 65 | print(' {}'.format(a)) 66 | 67 | return rc 68 | 69 | if __name__ == "__main__": 70 | try: 71 | sys.exit(main()) 72 | except KeyboardInterrupt: 73 | print('\nReceived Keyboard interrupt.') 74 | print('Exiting...') 75 | -------------------------------------------------------------------------------- /aws-cfn-control/awscfnctl/get_inst_from_asg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 7 | # except in compliance with the License. A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 12 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under the License. 14 | # 15 | 16 | import sys 17 | import boto3 18 | import argparse 19 | from awscfnctl import CfnControl 20 | 21 | progname = 'get_inst_from_asg' 22 | 23 | def arg_parse(): 24 | 25 | parser = argparse.ArgumentParser(prog=progname, description='List instances in an ASG') 26 | 27 | opt_group = parser.add_argument_group() 28 | opt_group.add_argument('-r', dest='region', required=False, help="Region name") 29 | 30 | req_group = parser.add_argument_group('required arguments') 31 | req_group.add_argument('-a', dest='asg_name', required=True) 32 | 33 | return parser.parse_args() 34 | 35 | 36 | def main(): 37 | 38 | rc = 0 39 | 40 | args = arg_parse() 41 | 42 | region = args.region 43 | asg = args.asg_name 44 | 45 | cfn_client = CfnControl(region=region) 46 | 47 | instances = cfn_client.get_inst_from_asg(asg) 48 | 49 | for i in instances: 50 | print(' {}'.format(i)) 51 | 52 | asg_status = cfn_client.ck_asg_inst_status(asg) 53 | 54 | return rc 55 | 56 | if __name__ == "__main__": 57 | try: 58 | sys.exit(main()) 59 | except KeyboardInterrupt: 60 | print('\nReceived Keyboard interrupt.') 61 | print('Exiting...') 62 | 63 | -------------------------------------------------------------------------------- /aws-cfn-control/awscfnctl/get_priv_dns_asg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2013-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the 6 | # License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/apache2.0/ 9 | # 10 | # or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 11 | # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | 15 | import sys 16 | import boto3 17 | import argparse 18 | 19 | progname = 'get_priv_dns_asg' 20 | 21 | def arg_parse(): 22 | 23 | 24 | parser = argparse.ArgumentParser(prog=progname, 25 | description='Print instance info from an ASG', 26 | epilog='Example: {} -s -r '.format(progname) 27 | ) 28 | 29 | parser.add_argument('-i', dest='print_inst_id', action='store_true', 30 | help='Print the instance IDs with the private DNS names' 31 | ) 32 | 33 | req_group = parser.add_argument_group('required arguments') 34 | 35 | # required arguments 36 | req_group.add_argument('-a', dest='asg', required="True") 37 | req_group.add_argument('-r', dest='region', required="True") 38 | 39 | return parser.parse_args() 40 | 41 | 42 | def main(): 43 | 44 | args = arg_parse() 45 | 46 | region = args.region 47 | asg = args.asg 48 | 49 | asg_client = boto3.client('autoscaling', region_name=region) 50 | asg_response = asg_client.describe_auto_scaling_groups(AutoScalingGroupNames=[asg]) 51 | ec2 = boto3.resource('ec2', region_name=region) 52 | 53 | # Build instance list 54 | for r in asg_response['AutoScalingGroups']: 55 | for i in r['Instances']: 56 | if args.print_inst_id: 57 | print('{0} {1}'.format( 58 | ec2.Instance(i['InstanceId']).instance_id, 59 | ec2.Instance(i['InstanceId']).private_dns_name.replace('.ec2.internal', '')) 60 | ) 61 | else: 62 | print(ec2.Instance(i['InstanceId']).private_dns_name.replace('.ec2.internal', '')) 63 | 64 | 65 | if __name__ == "__main__": 66 | try: 67 | sys.exit(main()) 68 | except KeyboardInterrupt: 69 | print('\nReceived Keyboard interrupt.') 70 | print('Exiting...') 71 | except ValueError as e: 72 | print('ERROR: {0}'.format(e)) 73 | 74 | -------------------------------------------------------------------------------- /aws-cfn-control/awscfnctl/getamiinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 7 | # except in compliance with the License. A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 12 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under the License. 14 | # 15 | 16 | import sys 17 | import boto3 18 | import argparse 19 | 20 | 21 | _PROPS = [ 22 | 'Name', 23 | 'ImageId', 24 | 'ImageType', 25 | 'ImageLocation', 26 | 'State', 27 | 'OwnerId', 28 | 'Description', 29 | 'EnaSupport', 30 | 'SriovNetSupport', 31 | 'VirtualizationType', 32 | 'Hypervisor', 33 | 'Architecture', 34 | 'RootDeviceType', 35 | 'CreationDate', 36 | 'Public', 37 | 'ProductCodeId', 38 | 'ImageOwnerAlias', 39 | 'BlockDeviceMappings', 40 | 'Architecture', 41 | 'RootDeviceType', 42 | 'RootDeviceName', 43 | 'Public', 44 | ] 45 | 46 | _BlockDevice_INFO = [ 47 | 'DeviceName', 48 | 'SnapshotId', 49 | 'DeleteOnTermination', 50 | 'VolumeType', 51 | 'VolumeSize', 52 | 'Encrypted', 53 | ] 54 | 55 | 56 | def arg_parse(): 57 | parser = argparse.ArgumentParser(prog='get_ami_id') 58 | 59 | req_group = parser.add_argument_group('required arguments') 60 | req_group.add_argument('-i', dest='ami_id', help='AMI ID (default region is us-east-1)', required=True ) 61 | 62 | opt_group = parser.add_argument_group('optional arguments') 63 | opt_group.add_argument('-r', dest='region', required=False, help="Region name (default is us-east-1)") 64 | 65 | return parser.parse_args() 66 | 67 | 68 | def image_info(client, owners, ami_name): 69 | 70 | response = client.describe_images( 71 | DryRun=False, 72 | Owners=[ 73 | owners, 74 | ], 75 | Filters=[ 76 | { 77 | 'Name': 'name', 78 | 'Values': [ 79 | ami_name, 80 | ] 81 | }, 82 | ] 83 | ) 84 | 85 | return response 86 | 87 | def get_image_info(client, ami_id): 88 | 89 | response = client.describe_images( 90 | DryRun=False, 91 | ImageIds=[ 92 | ami_id, 93 | ], 94 | ) 95 | 96 | resp = dict() 97 | 98 | for p in _PROPS: 99 | try: 100 | resp[p] = response["Images"][0][p] 101 | if resp[p] == 1: 102 | resp[p] = "True" 103 | elif resp[p] == 0: 104 | resp[p] = "False" 105 | except KeyError: 106 | resp[p] = "NO VALUE FOUND" 107 | 108 | return resp 109 | 110 | 111 | def print_image_info(ami, client): 112 | 113 | resp = dict() 114 | resp = get_image_info(client, ami) 115 | 116 | for k in _PROPS: 117 | if k == "BlockDeviceMappings": 118 | if type(resp[k]) is list: 119 | for blk_devs in resp[k]: 120 | block_dev = dict(blk_devs) 121 | for dev in block_dev.keys(): 122 | if type(block_dev[dev]) is str: 123 | print(" {0:<20}: {1:<30} {2:<30}".format(k, dev, block_dev[dev])) 124 | elif type(block_dev[dev]) is dict: 125 | if dev == "Ebs": 126 | print(" {0:<20}: {1:<30} {2:<30}".format(k, 'Block Device Type', dev)) 127 | for dev_info in block_dev[dev].keys(): 128 | print(" {0:<20}: {1:<30} {2:<30}".format(k, dev_info, block_dev[dev][dev_info])) 129 | else: 130 | print(" {0:<20}: {1:<30}".format(k, resp[k])) 131 | 132 | 133 | def main(): 134 | 135 | rc = 0 136 | 137 | args = arg_parse() 138 | region = args.region 139 | ami = args.ami_id 140 | 141 | if region == "": 142 | region = 'us-east-1' 143 | 144 | client = boto3.client('ec2', region_name=region) 145 | print("Checking region {0} for AMI info...".format(region)) 146 | print_image_info(ami, client) 147 | 148 | return rc 149 | 150 | 151 | if __name__ == "__main__": 152 | try: 153 | sys.exit(main()) 154 | except KeyboardInterrupt: 155 | print('\nReceived Keyboard interrupt.') 156 | print('Exiting...') 157 | except ValueError as e: 158 | print('ERROR: {0}'.format(e)) 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /aws-cfn-control/awscfnctl/getec2keys.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 7 | # except in compliance with the License. A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 12 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under the License. 14 | # 15 | 16 | import boto3 17 | 18 | ec2 = boto3.client('ec2') 19 | response = ec2.describe_key_pairs() 20 | for pair in (response['KeyPairs']): 21 | print(pair['KeyName']) 22 | -------------------------------------------------------------------------------- /aws-cfn-control/awscfnctl/getinstinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | #!/usr/bin/env python 4 | 5 | # 6 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 9 | # except in compliance with the License. A copy of the License is located at 10 | # 11 | # http://aws.amazon.com/apache2.0/ 12 | # 13 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 14 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations under the License. 16 | # 17 | 18 | import sys 19 | import argparse 20 | import datetime 21 | from awscfnctl import CfnControl 22 | 23 | def prRed(prt): return("\033[91m{}\033[00m".format(prt)) 24 | def prGreen(prt): return("\033[92m{}\033[00m".format(prt)) 25 | def prYellow(prt): return("\033[93m{}\033[00m".format(prt)) 26 | def prLightPurple(prt): return("\033[94m{}\033[00m".format(prt)) 27 | def prPurple(prt): return("\033[95m{}\033[00m".format(prt)) 28 | def prCyan(prt): return("\033[96m{}\033[00m".format(prt)) 29 | def prLightGray(prt): return("\033[97m{}\033[00m".format(prt)) 30 | def prBlack(prt): return("\033[98m{}\033[00m".format(prt)) 31 | 32 | 33 | progname = 'getinstinfo' 34 | 35 | 36 | def Sort(sub_li): 37 | 38 | # reverse = None (Sorts in Ascending order) 39 | # key is set to sort using second element of 40 | # sublist lambda has been used 41 | sub_li.sort(key = lambda x: x[1]) 42 | return sub_li 43 | 44 | 45 | def print_header(): 46 | 47 | print('{:<20} {:<20} {:<20.20} {:<30} {:<15} {:<7} {:<15} {:<20}'.format( 48 | 'Instance ID', 49 | 'Launch Date', 50 | 'Name', 51 | 'Internal DNS', 52 | 'Internal IP', 53 | 'State', 54 | 'Public IP', 55 | 'Instance type' 56 | )) 57 | 58 | 59 | def arg_parse(): 60 | 61 | parser = argparse.ArgumentParser(prog=progname, description='Get instance info') 62 | 63 | opt_group = parser.add_argument_group() 64 | opt_group.add_argument('-s', dest='instance_state', required=False, 65 | help='Instance State (pending | running | shutting-down | terminated | stopping | stopped)' 66 | ) 67 | 68 | req_group = parser.add_argument_group('required arguments') 69 | req_group.add_argument('-r', dest='region', required=True) 70 | 71 | return parser.parse_args() 72 | 73 | def main(): 74 | 75 | rc = 0 76 | 77 | args = arg_parse() 78 | region = args.region 79 | instance_state = args.instance_state 80 | 81 | inst_info_all = list() 82 | 83 | client = CfnControl(region=region) 84 | for inst, info in client.get_instance_info(instance_state=instance_state).items(): 85 | inst_info = list() 86 | inst_info.append(inst) 87 | for k, v in info.items(): 88 | if isinstance(v, datetime.datetime): 89 | v = str(v)[:-6] 90 | inst_info.append(v) 91 | inst_info_all.append(inst_info) 92 | 93 | print 94 | print_header() 95 | print(155 * '-') 96 | print('\n'.join('{:<20} {:<20} {:<20.20} {:<30} {:<15} {:<7} {:<15} {:<20}'.format(*i) for i in Sort(inst_info_all))) 97 | print 98 | 99 | return rc 100 | 101 | if __name__ == "__main__": 102 | try: 103 | sys.exit(main()) 104 | except KeyboardInterrupt: 105 | print('\nReceived Keyboard interrupt.') 106 | print('Exiting...') 107 | except ValueError as e: 108 | print('ERROR: {0}'.format(e)) 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /aws-cfn-control/awscfnctl/getnetinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 7 | # except in compliance with the License. A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 12 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under the License. 14 | # 15 | 16 | import sys 17 | import argparse 18 | from awscfnctl import CfnControl 19 | 20 | progname = 'getnetinfo' 21 | 22 | 23 | def arg_parse(): 24 | 25 | parser = argparse.ArgumentParser(prog=progname, description='Launch a stack, with a config file') 26 | 27 | opt_group = parser.add_argument_group() 28 | opt_group.add_argument('-r', dest='region', required=False, help="Region name") 29 | 30 | return parser.parse_args() 31 | 32 | 33 | def get_subnets(client, vpc): 34 | 35 | all_subnets = client.get_subnets_from_vpc(vpc) 36 | 37 | subnet_ids = list() 38 | for subnet_id, subnet_info in all_subnets.items(): 39 | subnet_ids.append(subnet_id) 40 | 41 | return ' | '.join(subnet_ids) 42 | 43 | 44 | def get_sec_groups(client, vpc): 45 | 46 | all_security_group_info = client.get_security_groups(vpc=vpc) 47 | 48 | output_count = 0 49 | security_groups = list() 50 | for r in all_security_group_info: 51 | seg_group_with_name = '{0} ({1:.20})'.format(r['GroupId'], r['GroupName']) 52 | output_count += 1 53 | if output_count == 3: 54 | security_groups.append('{0:38.36}\n{1}'.format(seg_group_with_name, " "*19)) 55 | output_count = 0 56 | else: 57 | security_groups.append('{0:38.36}'.format(seg_group_with_name)) 58 | 59 | return ' '.join(security_groups) 60 | 61 | 62 | def main(): 63 | 64 | rc = 0 65 | 66 | args = arg_parse() 67 | region = args.region 68 | 69 | client = CfnControl(region=region) 70 | 71 | vpc_keys_to_print = [ 72 | 'Tag_Name', 73 | 'IsDefault', 74 | 'CidrBlock', 75 | ] 76 | 77 | all_vpcs = client.get_vpcs() 78 | 79 | for vpc_id, vpc_info in all_vpcs.items(): 80 | lines = '=' * len(vpc_id) 81 | print('{0}\n{1}\n{2}'.format(lines, vpc_id, lines)) 82 | print(' Subnets: {0}'.format(get_subnets(client,vpc_id))) 83 | for vpc_k in vpc_keys_to_print: 84 | try: 85 | print(' {0} = {1}'.format(vpc_k, vpc_info[vpc_k])) 86 | except KeyError: 87 | pass 88 | print(' Security Groups: {0}'.format(get_sec_groups(client,vpc_id))) 89 | print("") 90 | 91 | return rc 92 | 93 | if __name__ == "__main__": 94 | try: 95 | sys.exit(main()) 96 | except KeyboardInterrupt: 97 | print('\nReceived Keyboard interrupt.') 98 | print('Exiting...') 99 | except ValueError as e: 100 | print('ERROR: {0}'.format(e)) 101 | 102 | 103 | -------------------------------------------------------------------------------- /aws-cfn-control/awscfnctl/getstackinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 7 | # except in compliance with the License. A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 12 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under the License. 14 | # 15 | 16 | import sys 17 | import time 18 | import argparse 19 | from awscfnctl import CfnControl 20 | 21 | progname = 'getstackinfo' 22 | 23 | 24 | def arg_parse(): 25 | 26 | parser = argparse.ArgumentParser(prog=progname, description='Get information for a stack') 27 | 28 | opt_group = parser.add_argument_group() 29 | opt_group.add_argument('-r', dest='region', required=False) 30 | 31 | req_group = parser.add_argument_group('required arguments') 32 | req_group.add_argument('-s', dest='stack_name', required=True) 33 | 34 | return parser.parse_args() 35 | 36 | 37 | def get_stack_events(client, stack_name): 38 | 39 | try: 40 | paginator = client.get_paginator('describe_stack_events') 41 | pages = paginator.paginate(StackName=stack_name, PaginationConfig={'MaxItems': 100}) 42 | return next(iter(pages))["StackEvents"] 43 | except Exception as e: 44 | raise ValueError(e) 45 | 46 | 47 | def main(): 48 | 49 | rc = 0 50 | 51 | args = arg_parse() 52 | region = args.region 53 | stack_name = args.stack_name 54 | 55 | 56 | client = CfnControl(region=region) 57 | client.get_stack_info(stack_name=stack_name) 58 | 59 | all_events = list() 60 | 61 | events = True 62 | 63 | while events: 64 | stk_status = get_stack_events(client.client_cfn, stack_name) 65 | 66 | for s in reversed(stk_status): 67 | event_id = s['EventId'] 68 | if event_id not in all_events: 69 | all_events.append(event_id) 70 | try: 71 | print('{0} {1} {2}'.format(s['LogicalResourceId'], s['ResourceStatus'], s['ResourceStatusReason'])) 72 | except KeyError: 73 | print('{0} {1}'.format(s['LogicalResourceId'], s['ResourceStatus'])) 74 | except Exception as e: 75 | raise ValueError(e) 76 | 77 | if s['LogicalResourceId'] == stack_name and s['ResourceStatus'] == 'ROLLBACK_COMPLETE': 78 | events = False 79 | time.sleep(1) 80 | 81 | return rc 82 | 83 | if __name__ == "__main__": 84 | try: 85 | sys.exit(main()) 86 | except KeyboardInterrupt: 87 | print('\nReceived Keyboard interrupt.') 88 | print('Exiting...') 89 | except ValueError as e: 90 | print('ERROR: {0}'.format(e)) 91 | 92 | 93 | -------------------------------------------------------------------------------- /aws-cfn-control/setup.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-cfn-control/29e5d602a82b66ddeca3b6761587804a23c6ae3d/aws-cfn-control/setup.cfg -------------------------------------------------------------------------------- /aws-cfn-control/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | aws-cfn-control 3 | --------------- 4 | """ 5 | 6 | # 7 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 8 | # 9 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 10 | # except in compliance with the License. A copy of the License is located at 11 | # 12 | # http://aws.amazon.com/apache2.0/ 13 | # 14 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 15 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | # License for the specific language governing permissions and limitations under the License. 17 | # 18 | 19 | # setup.py classifiers 20 | # https://pypi.python.org/pypi?%3Aaction=list_classifiers 21 | 22 | import os 23 | import io 24 | from setuptools import setup, find_packages 25 | 26 | 27 | def open_file(fname): 28 | return open(os.path.join(os.path.dirname(__file__), fname)) 29 | 30 | 31 | _version = "1.0.11" 32 | 33 | console_scripts = [ 'cfnctl = awscfnctl.cfnctl:main', 34 | 'getamiinfo = awscfnctl.getamiinfo:main', 35 | 'build_ami_maps = awscfnctl.build_ami_maps:main' 36 | ] 37 | 38 | 39 | # read the contents of your README file 40 | this_directory = os.path.abspath(os.path.dirname(__file__)) 41 | with io.open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f: 42 | long_description = f.read() 43 | 44 | setup( 45 | name='aws-cfn-control', 46 | version=_version, 47 | url='https://github.com/awslabs/aws-cfn-control', 48 | license="Apache License 2.0", 49 | author='Mark Duffield', 50 | author_email='duff@amazon.com', 51 | description='Command line launch and management tool for AWS CloudFormation', 52 | long_description=long_description, 53 | long_description_content_type='text/markdown', 54 | zip_safe=False, 55 | include_package_data=True, 56 | install_requires=[ 57 | 'cfn-flip', 58 | 'boto3>=1.4.7', 59 | ], 60 | packages=find_packages(), 61 | keywords='aws cfn control cloudformation stack', 62 | entry_points=dict(console_scripts=console_scripts), 63 | classifiers=[ 64 | 'Intended Audience :: Developers', 65 | 'Natural Language :: English', 66 | 'Environment :: Web Environment', 67 | 'License :: OSI Approved :: Apache Software License', 68 | 'Operating System :: OS Independent', 69 | 'Development Status :: 1 - Planning', 70 | 'Programming Language :: Python', 71 | 'Topic :: Software Development :: Libraries :: Python Modules', 72 | 'Topic :: Utilities' 73 | ] 74 | ) 75 | -------------------------------------------------------------------------------- /copyright.template.json: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Copyright [first edit year]-[latest edit year] Amazon.com, Inc. or its affiliates. All 4 | # Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 7 | # except in compliance with the License. A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 12 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under the License. 14 | # 15 | 16 | 17 | { 18 | "license": "awslabs", 19 | "year": "2017" 20 | } 21 | 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include requirements.txt -------------------------------------------------------------------------------- /troposhpere/Instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 7 | # except in compliance with the License. A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 12 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under the License. 14 | # 15 | 16 | import sys 17 | from pprint import pprint 18 | 19 | from troposphere import Base64, FindInMap, GetAtt, Join, iam, Tags 20 | from troposphere import Parameter, Output, Ref, Template, Condition, Equals, And, Or, Not, If 21 | from troposphere.cloudformation import WaitCondition, WaitConditionHandle 22 | from troposphere import cloudformation, autoscaling 23 | from troposphere.autoscaling import AutoScalingGroup, Tag, Metadata 24 | from troposphere.autoscaling import LaunchConfiguration 25 | from troposphere.elasticloadbalancing import LoadBalancer 26 | from troposphere.policies import ( 27 | AutoScalingReplacingUpdate, AutoScalingRollingUpdate, UpdatePolicy 28 | ) 29 | import troposphere.ec2 as ec2 30 | from troposphere.ec2 import PortRange, NetworkAcl, Route, \ 31 | VPCGatewayAttachment, SubnetRouteTableAssociation, Subnet, RouteTable, \ 32 | VPC, NetworkInterfaceProperty, NetworkAclEntry, \ 33 | SubnetNetworkAclAssociation, EIP, Instance, InternetGateway, \ 34 | SecurityGroupRule, SecurityGroup, SecurityGroupIngress, SecurityGroupEgress 35 | import troposphere.elasticloadbalancing as elb 36 | from troposphere.helpers import userdata 37 | from troposphere.iam import AccessKey, Group, LoginProfile, PolicyType, Role, InstanceProfile, User 38 | from troposphere import GetAtt, Ref, Template 39 | from troposphere.iam import LoginProfile, Policy, User 40 | from troposphere.efs import FileSystem, MountTarget 41 | 42 | from troposphere.policies import CreationPolicy, ResourceSignal 43 | 44 | 45 | def main(): 46 | 47 | t = Template() 48 | t.set_description("test instance launch") 49 | t.set_version("2010-09-09") 50 | 51 | InstUserData = [ 52 | '#!/usr/bin/env bash\n', 53 | '\n', 54 | 'set -x\n', 55 | '\n', 56 | 'my_wait_handle="', Ref('InstanceWaitHandle'), '"\n', 57 | 'curl -X PUT -H \'Content-Type:\' --data-binary \'{ "Status" : "SUCCESS", "Reason" : "Instance launched", "UniqueId" : "launch001", "Data" : "Instance launched."}\' "${my_wait_handle}"', '\n', 58 | '\n', 59 | ] 60 | 61 | EC2KeyName = t.add_parameter(Parameter( 62 | 'EC2KeyName', 63 | Type="AWS::EC2::KeyPair::KeyName", 64 | Description="Name of an existing EC2 KeyPair to enable SSH access to the instance.", 65 | ConstraintDescription="REQUIRED: Must be a valud EC2 key pair", 66 | )) 67 | 68 | OperatingSystem = t.add_parameter(Parameter( 69 | 'OperatingSystem', 70 | Type="String", 71 | Description="Operating System", 72 | Default="centos7", 73 | AllowedValues=[ 74 | "alinux2", 75 | "centos7", 76 | "rhel7", 77 | ], 78 | ConstraintDescription="Must be: alinux2, centos7, rhel7" 79 | )) 80 | 81 | myInstanceType = t.add_parameter(Parameter( 82 | 'MyInstanceType', 83 | Type="String", 84 | Description="Instance type", 85 | Default="m5.2xlarge", 86 | )) 87 | 88 | VpcId = t.add_parameter(Parameter( 89 | 'VpcId', 90 | Type="AWS::EC2::VPC::Id", 91 | Description="VPC Id for this instance", 92 | )) 93 | 94 | Subnet = t.add_parameter(Parameter( 95 | 'Subnet', 96 | Type="AWS::EC2::Subnet::Id", 97 | Description="Subnet IDs" 98 | )) 99 | 100 | ExistingSecurityGroup = t.add_parameter(Parameter( 101 | 'ExistingSecurityGroup', 102 | Type="AWS::EC2::SecurityGroup::Id", 103 | Description="OPTIONAL: Choose an existing Security Group ID, e.g. sg-abcd1234" 104 | )) 105 | 106 | UsePublicIp = t.add_parameter(Parameter( 107 | 'UsePublicIp', 108 | Type="String", 109 | Description="Should a public IP address be given to the instance", 110 | Default="true", 111 | ConstraintDescription="true/false", 112 | AllowedValues=[ 113 | "true", 114 | "false" 115 | ] 116 | )) 117 | 118 | SshAccessCidr = t.add_parameter(Parameter( 119 | 'SshAccessCidr', 120 | Type="String", 121 | Description="CIDR Block for SSH access, default 127.0.0.1/32", 122 | Default="127.0.0.1/32", 123 | AllowedPattern="(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", 124 | ConstraintDescription="Must be a valid CIDR x.x.x.x/x" 125 | )) 126 | 127 | RootRole = t.add_resource(iam.Role( 128 | "RootRole", 129 | AssumeRolePolicyDocument={"Statement": [{ 130 | "Effect": "Allow", 131 | "Principal": { 132 | "Service": [ "ec2.amazonaws.com" ] 133 | }, 134 | "Action": [ "sts:AssumeRole" ] 135 | }]} 136 | )) 137 | 138 | SshSecurityGroup = t.add_resource(SecurityGroup( 139 | "SshSecurityGroup", 140 | VpcId = Ref(VpcId), 141 | GroupDescription = "SSH Secuirty group", 142 | SecurityGroupIngress=[ 143 | ec2.SecurityGroupRule( 144 | IpProtocol="tcp", 145 | FromPort="22", 146 | ToPort="22", 147 | CidrIp=Ref(SshAccessCidr), 148 | ), 149 | ] 150 | )) 151 | 152 | RootInstanceProfile = t.add_resource(InstanceProfile( 153 | "RootInstanceProfile", 154 | Roles=[Ref(RootRole)] 155 | )) 156 | 157 | tags = Tags(Name=Ref("AWS::StackName")) 158 | 159 | myInstance = t.add_resource(ec2.Instance( 160 | 'MyInstance', 161 | ImageId=FindInMap("AWSRegionAMI", Ref("AWS::Region"), Ref(OperatingSystem)), 162 | KeyName=Ref(EC2KeyName), 163 | InstanceType=(Ref(myInstanceType)), 164 | NetworkInterfaces=[ 165 | NetworkInterfaceProperty( 166 | GroupSet=If( 167 | "not_existing_sg", 168 | [Ref(SshSecurityGroup)], 169 | [Ref(SshSecurityGroup), Ref(ExistingSecurityGroup)] 170 | ), 171 | AssociatePublicIpAddress=Ref(UsePublicIp), 172 | DeviceIndex='0', 173 | DeleteOnTermination='true', 174 | SubnetId=Ref(Subnet))], 175 | IamInstanceProfile=(Ref(RootInstanceProfile)), 176 | UserData=Base64(Join('', InstUserData)), 177 | )) 178 | 179 | t.add_mapping('AWSRegionAMI', { 180 | "ap-northeast-1": { 181 | "centos7": "ami-8e8847f1", 182 | "rhel7": "ami-6b0d5f0d" 183 | }, 184 | "ap-northeast-2": { 185 | "centos7": "ami-bf9c36d1", 186 | "rhel7": "ami-3eee4150" 187 | }, 188 | "ap-south-1": { 189 | "centos7": "ami-1780a878", 190 | "rhel7": "ami-5b673c34" 191 | }, 192 | "ap-southeast-1": { 193 | "centos7": "ami-8e0205f2", 194 | "rhel7": "ami-76144b0a" 195 | }, 196 | "ap-southeast-2": { 197 | "centos7": "ami-d8c21dba", 198 | "rhel7": "ami-67589505" 199 | }, 200 | "ca-central-1": { 201 | "centos7": "ami-e802818c", 202 | "rhel7": "ami-49f0762d" 203 | }, 204 | "eu-central-1": { 205 | "centos7": "ami-dd3c0f36", 206 | "rhel7": "ami-c86c3f23" 207 | }, 208 | "eu-west-1": { 209 | "centos7": "ami-3548444c", 210 | "rhel7": "ami-7c491f05" 211 | }, 212 | "eu-west-2": { 213 | "centos7": "ami-00846a67", 214 | "rhel7": "ami-7c1bfd1b" 215 | }, 216 | "eu-west-3": { 217 | "centos7": "ami-262e9f5b", 218 | "rhel7": "ami-5026902d" 219 | }, 220 | "sa-east-1": { 221 | "centos7": "ami-cb5803a7", 222 | "rhel7": "ami-b0b7e3dc" 223 | }, 224 | "us-east-1": { 225 | "centos7": "ami-9887c6e7", 226 | "rhel7": "ami-6871a115" 227 | }, 228 | "us-east-2": { 229 | "centos7": "ami-9c0638f9", 230 | "rhel7": "ami-03291866" 231 | }, 232 | "us-west-1": { 233 | "centos7": "ami-4826c22b", 234 | "rhel7": "ami-18726478" 235 | }, 236 | "us-west-2": { 237 | "centos7": "ami-3ecc8f46", 238 | "rhel7": "ami-28e07e50" 239 | } 240 | }) 241 | 242 | t.add_condition( 243 | "not_existing_sg", 244 | Equals(Ref(ExistingSecurityGroup), "") 245 | ) 246 | 247 | t.add_condition( 248 | "Has_Public_Ip", 249 | Equals(Ref(UsePublicIp), "true") 250 | ) 251 | 252 | mywaithandle = t.add_resource(WaitConditionHandle('InstanceWaitHandle')) 253 | 254 | mywaitcondition = t.add_resource(WaitCondition( 255 | "InstanceWaitCondition", 256 | Handle=Ref(mywaithandle), 257 | Timeout="1500", 258 | DependsOn="MyInstance" 259 | )) 260 | 261 | t.add_output([ 262 | Output( 263 | "InstanceID", 264 | Description="Instance ID", 265 | Value=Ref(myInstance) 266 | ) 267 | ]) 268 | 269 | t.add_output([ 270 | Output( 271 | "InstancePrivateIP", 272 | Value=GetAtt('MyInstance', 'PrivateIp') 273 | ) 274 | ]) 275 | 276 | t.add_output([ 277 | Output( 278 | "InstancePublicIP", 279 | Value=GetAtt('MyInstance', 'PublicIp'), 280 | Condition="Has_Public_Ip" 281 | ) 282 | ]) 283 | 284 | ##print(t.to_yaml()) 285 | print(t.to_json(indent=2)) 286 | 287 | 288 | if __name__ == "__main__": 289 | sys.exit(main()) 290 | -------------------------------------------------------------------------------- /troposhpere/VPC_2x_AZ.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file 7 | # except in compliance with the License. A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" 12 | # BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under the License. 14 | # 15 | 16 | from troposphere import Base64, Select, FindInMap, GetAtt, GetAZs, Join, Output, If, And, Not, Or, Equals, Condition 17 | from troposphere import Parameter, Ref, Tags, Template 18 | from troposphere.cloudformation import Init 19 | from troposphere.cloudfront import Distribution, DistributionConfig 20 | from troposphere.cloudfront import Origin, DefaultCacheBehavior 21 | from troposphere.ec2 import PortRange 22 | from troposphere.ec2 import SubnetRouteTableAssociation 23 | from troposphere.ec2 import InternetGateway 24 | from troposphere.ec2 import Route 25 | from troposphere.ec2 import RouteTable 26 | from troposphere.ec2 import NatGateway 27 | from troposphere.ec2 import VPC 28 | from troposphere.ec2 import Subnet 29 | from troposphere.ec2 import EIP 30 | from troposphere.ec2 import VPCGatewayAttachment 31 | from troposphere.ec2 import SecurityGroup 32 | 33 | 34 | t = Template() 35 | 36 | t.set_description("""\ 37 | This template deploys a VPC, with a pair of public and private subnets spread across two Availability Zones. It deploys an Internet Gateway, with a default route on the public subnets. It deploys a pair of NAT Gateways (one in each AZ), and default routes for them in the private subnets.""") 38 | 39 | 40 | EnvironmentName = t.add_parameter(Parameter( 41 | "EnvironmentName", 42 | Type="String", 43 | Default="vpc-stack", 44 | Description="An environment name that will be prefixed to resource names", 45 | )) 46 | 47 | PrivateSubnet1CIDR = t.add_parameter(Parameter( 48 | "PrivateSubnet1CIDR", 49 | Default="10.192.20.0/24", 50 | Type="String", 51 | Description="Please enter the IP range (CIDR notation) for the private subnet in the first Availability Zone", 52 | )) 53 | 54 | PrivateSubnet2CIDR = t.add_parameter(Parameter( 55 | "PrivateSubnet2CIDR", 56 | Default="10.192.21.0/24", 57 | Type="String", 58 | Description="Please enter the IP range (CIDR notation) for the private subnet in the second Availability Zone", 59 | )) 60 | 61 | PublicSubnet1CIDR = t.add_parameter(Parameter( 62 | "PublicSubnet1CIDR", 63 | Default="10.192.10.0/24", 64 | Type="String", 65 | Description="Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone", 66 | )) 67 | 68 | PublicSubnet2CIDR = t.add_parameter(Parameter( 69 | "PublicSubnet2CIDR", 70 | Default="10.192.11.0/24", 71 | Type="String", 72 | Description="Please enter the IP range (CIDR notation) for the public subnet in the second Availability Zone", 73 | )) 74 | 75 | VpcCIDR = t.add_parameter(Parameter( 76 | "VpcCIDR", 77 | Default="10.192.0.0/16", 78 | Type="String", 79 | Description="Please enter the IP range (CIDR notation) for this VPC", 80 | )) 81 | 82 | AZ1 = t.add_parameter(Parameter( 83 | "AZ1", 84 | Type="AWS::EC2::AvailabilityZone::Name", 85 | Description="Please choose AZ1" 86 | )) 87 | 88 | AZ2 = t.add_parameter(Parameter( 89 | "AZ2", 90 | Type="AWS::EC2::AvailabilityZone::Name", 91 | Description="Please choose AZ2" 92 | )) 93 | 94 | PublicSubnet2RouteTableAssociation = t.add_resource(SubnetRouteTableAssociation( 95 | "PublicSubnet2RouteTableAssociation", 96 | SubnetId=Ref("PublicSubnet2"), 97 | RouteTableId=Ref("PublicRouteTable"), 98 | )) 99 | 100 | InternetGateway = t.add_resource(InternetGateway( 101 | "InternetGateway", 102 | Tags=Tags( 103 | Name=Ref(EnvironmentName), 104 | ), 105 | )) 106 | 107 | DefaultPublicRoute = t.add_resource(Route( 108 | "DefaultPublicRoute", 109 | GatewayId=Ref(InternetGateway), 110 | DestinationCidrBlock="0.0.0.0/0", 111 | RouteTableId=Ref("PublicRouteTable"), 112 | DependsOn="InternetGatewayAttachment", 113 | )) 114 | 115 | DefaultPrivateRoute1 = t.add_resource(Route( 116 | "DefaultPrivateRoute1", 117 | DestinationCidrBlock="0.0.0.0/0", 118 | RouteTableId=Ref("PrivateRouteTable1"), 119 | NatGatewayId=Ref("NatGateway1"), 120 | )) 121 | 122 | DefaultPrivateRoute2 = t.add_resource(Route( 123 | "DefaultPrivateRoute2", 124 | DestinationCidrBlock="0.0.0.0/0", 125 | RouteTableId=Ref("PrivateRouteTable2"), 126 | NatGatewayId=Ref("NatGateway2"), 127 | )) 128 | 129 | PrivateSubnet2RouteTableAssociation = t.add_resource(SubnetRouteTableAssociation( 130 | "PrivateSubnet2RouteTableAssociation", 131 | SubnetId=Ref("PrivateSubnet2"), 132 | RouteTableId=Ref("PrivateRouteTable2"), 133 | )) 134 | 135 | PrivateRouteTable2 = t.add_resource(RouteTable( 136 | "PrivateRouteTable2", 137 | VpcId=Ref("VPCId"), 138 | Tags=Tags( 139 | Name={ "Fn::Sub": "${EnvironmentName} Private Routes (AZ2)" }, 140 | ), 141 | )) 142 | 143 | NatGateway1 = t.add_resource(NatGateway( 144 | "NatGateway1", 145 | SubnetId=Ref("PublicSubnet1"), 146 | AllocationId=GetAtt("NatGateway1EIP", "AllocationId"), 147 | )) 148 | 149 | NatGateway2 = t.add_resource(NatGateway( 150 | "NatGateway2", 151 | SubnetId=Ref("PublicSubnet2"), 152 | AllocationId=GetAtt("NatGateway2EIP", "AllocationId"), 153 | )) 154 | 155 | PrivateRouteTable1 = t.add_resource(RouteTable( 156 | "PrivateRouteTable1", 157 | VpcId=Ref("VPCId"), 158 | Tags=Tags( 159 | Name={ "Fn::Sub": "${EnvironmentName} Private Routes (AZ1)" }, 160 | ), 161 | )) 162 | 163 | VPCId = t.add_resource(VPC( 164 | "VPCId", 165 | CidrBlock=Ref(VpcCIDR), 166 | EnableDnsSupport="true", 167 | EnableDnsHostnames="true", 168 | Tags=Tags( 169 | Name=Ref(EnvironmentName), 170 | ), 171 | )) 172 | 173 | PrivateSubnet1 = t.add_resource(Subnet( 174 | "PrivateSubnet1", 175 | Tags=Tags( 176 | Name={ "Fn::Sub": "${EnvironmentName} Private Subnet (AZ1)" }, 177 | ), 178 | VpcId=Ref(VPCId), 179 | CidrBlock=Ref(PrivateSubnet1CIDR), 180 | MapPublicIpOnLaunch=False, 181 | AvailabilityZone=Ref(AZ1) 182 | )) 183 | 184 | PrivateSubnet2 = t.add_resource(Subnet( 185 | "PrivateSubnet2", 186 | Tags=Tags( 187 | Name={ "Fn::Sub": "${EnvironmentName} Private Subnet (AZ2)" }, 188 | ), 189 | VpcId=Ref(VPCId), 190 | CidrBlock=Ref(PrivateSubnet2CIDR), 191 | MapPublicIpOnLaunch=False, 192 | AvailabilityZone=Ref(AZ2) 193 | )) 194 | 195 | PublicSubnet1RouteTableAssociation = t.add_resource(SubnetRouteTableAssociation( 196 | "PublicSubnet1RouteTableAssociation", 197 | SubnetId=Ref("PublicSubnet1"), 198 | RouteTableId=Ref("PublicRouteTable"), 199 | )) 200 | 201 | NatGateway1EIP = t.add_resource(EIP( 202 | "NatGateway1EIP", 203 | Domain="vpc", 204 | DependsOn="InternetGatewayAttachment", 205 | )) 206 | 207 | PrivateSubnet1RouteTableAssociation = t.add_resource(SubnetRouteTableAssociation( 208 | "PrivateSubnet1RouteTableAssociation", 209 | SubnetId=Ref(PrivateSubnet1), 210 | RouteTableId=Ref(PrivateRouteTable1), 211 | )) 212 | 213 | PublicRouteTable = t.add_resource(RouteTable( 214 | "PublicRouteTable", 215 | VpcId=Ref(VPCId), 216 | Tags=Tags( 217 | Name={ "Fn::Sub": "${EnvironmentName} Public Routes" }, 218 | ), 219 | )) 220 | 221 | NatGateway2EIP = t.add_resource(EIP( 222 | "NatGateway2EIP", 223 | Domain="vpc", 224 | DependsOn="InternetGatewayAttachment", 225 | )) 226 | 227 | PublicSubnet1 = t.add_resource(Subnet( 228 | "PublicSubnet1", 229 | Tags=Tags( 230 | Name={ "Fn::Sub": "${EnvironmentName} Public Subnet (AZ1)" }, 231 | ), 232 | VpcId=Ref(VPCId), 233 | CidrBlock=Ref(PublicSubnet1CIDR), 234 | MapPublicIpOnLaunch=True, 235 | AvailabilityZone=Ref(AZ1) 236 | )) 237 | 238 | PublicSubnet2 = t.add_resource(Subnet( 239 | "PublicSubnet2", 240 | Tags=Tags( 241 | Name={ "Fn::Sub": "${EnvironmentName} Public Subnet (AZ2)" }, 242 | ), 243 | VpcId=Ref(VPCId), 244 | CidrBlock=Ref(PublicSubnet2CIDR), 245 | MapPublicIpOnLaunch=True, 246 | AvailabilityZone=Ref(AZ2) 247 | )) 248 | 249 | InternetGatewayAttachment = t.add_resource(VPCGatewayAttachment( 250 | "InternetGatewayAttachment", 251 | VpcId=Ref(VPCId), 252 | InternetGatewayId=Ref(InternetGateway), 253 | )) 254 | 255 | NoIngressSecurityGroup = t.add_resource(SecurityGroup( 256 | "NoIngressSecurityGroup", 257 | GroupName="no-ingress-sg", 258 | VpcId=Ref(VPCId), 259 | GroupDescription="Security group with no ingress rule", 260 | )) 261 | 262 | t.add_output([ 263 | Output( 264 | "PublicSubnet1", 265 | Description="A reference to the public subnet in the 1st Availability Zone", 266 | Value=Ref(PublicSubnet1), 267 | ), 268 | Output( 269 | "PublicSubnets", 270 | Description="A list of the public subnets", 271 | Value=Join(",", [Ref(PublicSubnet1), Ref(PublicSubnet2)]), 272 | ), 273 | Output( 274 | "PublicSubnet2", 275 | Description="A reference to the public subnet in the 2nd Availability Zone", 276 | Value=Ref(PublicSubnet2), 277 | ), 278 | Output( 279 | "PrivateSubnet1", 280 | Description="A reference to the private subnet in the 1st Availability Zone", 281 | Value=Ref(PrivateSubnet1), 282 | ), 283 | Output( 284 | "PrivateSubnet2", 285 | Description="A reference to the private subnet in the 2nd Availability Zone", 286 | Value=Ref(PrivateSubnet2), 287 | ), 288 | Output( 289 | "PrivateSubnets", 290 | Description="A list of the private subnets", 291 | Value=Join(",", [Ref(PrivateSubnet1), Ref(PrivateSubnet2)]), 292 | ), 293 | Output( 294 | "VPCId", 295 | Description="A reference to the created VPC", 296 | Value=Ref(VPCId), 297 | ), 298 | Output( 299 | "NoIngressSecurityGroup", 300 | Description="Security group with no ingress rule", 301 | Value=Ref(NoIngressSecurityGroup), 302 | ) 303 | ]) 304 | 305 | print(t.to_json(indent=2)) 306 | 307 | 308 | 309 | 310 | -------------------------------------------------------------------------------- /troposhpere/_include/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-cfn-control/29e5d602a82b66ddeca3b6761587804a23c6ae3d/troposhpere/_include/__init__.py -------------------------------------------------------------------------------- /troposhpere/instance.template: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Conditions": { 4 | "Has_Public_Ip": { 5 | "Fn::Equals": [ 6 | { 7 | "Ref": "UsePublicIp" 8 | }, 9 | "true" 10 | ] 11 | }, 12 | "not_existing_sg": { 13 | "Fn::Equals": [ 14 | { 15 | "Ref": "ExistingSecurityGroup" 16 | }, 17 | "" 18 | ] 19 | } 20 | }, 21 | "Description": "test instance launch", 22 | "Mappings": { 23 | "AWSRegionAMI": { 24 | "ap-northeast-1": { 25 | "centos7": "ami-8e8847f1", 26 | "rhel7": "ami-6b0d5f0d" 27 | }, 28 | "ap-northeast-2": { 29 | "centos7": "ami-bf9c36d1", 30 | "rhel7": "ami-3eee4150" 31 | }, 32 | "ap-south-1": { 33 | "centos7": "ami-1780a878", 34 | "rhel7": "ami-5b673c34" 35 | }, 36 | "ap-southeast-1": { 37 | "centos7": "ami-8e0205f2", 38 | "rhel7": "ami-76144b0a" 39 | }, 40 | "ap-southeast-2": { 41 | "centos7": "ami-d8c21dba", 42 | "rhel7": "ami-67589505" 43 | }, 44 | "ca-central-1": { 45 | "centos7": "ami-e802818c", 46 | "rhel7": "ami-49f0762d" 47 | }, 48 | "eu-central-1": { 49 | "centos7": "ami-dd3c0f36", 50 | "rhel7": "ami-c86c3f23" 51 | }, 52 | "eu-west-1": { 53 | "centos7": "ami-3548444c", 54 | "rhel7": "ami-7c491f05" 55 | }, 56 | "eu-west-2": { 57 | "centos7": "ami-00846a67", 58 | "rhel7": "ami-7c1bfd1b" 59 | }, 60 | "eu-west-3": { 61 | "centos7": "ami-262e9f5b", 62 | "rhel7": "ami-5026902d" 63 | }, 64 | "sa-east-1": { 65 | "centos7": "ami-cb5803a7", 66 | "rhel7": "ami-b0b7e3dc" 67 | }, 68 | "us-east-1": { 69 | "centos7": "ami-9887c6e7", 70 | "rhel7": "ami-6871a115" 71 | }, 72 | "us-east-2": { 73 | "centos7": "ami-9c0638f9", 74 | "rhel7": "ami-03291866" 75 | }, 76 | "us-west-1": { 77 | "centos7": "ami-4826c22b", 78 | "rhel7": "ami-18726478" 79 | }, 80 | "us-west-2": { 81 | "centos7": "ami-3ecc8f46", 82 | "rhel7": "ami-28e07e50" 83 | } 84 | } 85 | }, 86 | "Outputs": { 87 | "InstanceID": { 88 | "Description": "Instance ID", 89 | "Value": { 90 | "Ref": "MyInstance" 91 | } 92 | }, 93 | "InstancePrivateIP": { 94 | "Value": { 95 | "Fn::GetAtt": [ 96 | "MyInstance", 97 | "PrivateIp" 98 | ] 99 | } 100 | }, 101 | "InstancePublicIP": { 102 | "Condition": "Has_Public_Ip", 103 | "Value": { 104 | "Fn::GetAtt": [ 105 | "MyInstance", 106 | "PublicIp" 107 | ] 108 | } 109 | } 110 | }, 111 | "Parameters": { 112 | "EC2KeyName": { 113 | "ConstraintDescription": "REQUIRED: Must be a valud EC2 key pair", 114 | "Description": "Name of an existing EC2 KeyPair to enable SSH access to the instance.", 115 | "Type": "AWS::EC2::KeyPair::KeyName" 116 | }, 117 | "ExistingSecurityGroup": { 118 | "Description": "OPTIONAL: Choose an existing Security Group ID, e.g. sg-abcd1234", 119 | "Type": "AWS::EC2::SecurityGroup::Id" 120 | }, 121 | "MyInstanceType": { 122 | "Default": "m5.2xlarge", 123 | "Description": "Instance type", 124 | "Type": "String" 125 | }, 126 | "OperatingSystem": { 127 | "AllowedValues": [ 128 | "alinux2", 129 | "centos7", 130 | "rhel7" 131 | ], 132 | "ConstraintDescription": "Must be: alinux2, centos7, rhel7", 133 | "Default": "centos7", 134 | "Description": "Operating System", 135 | "Type": "String" 136 | }, 137 | "SshAccessCidr": { 138 | "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", 139 | "ConstraintDescription": "Must be a valid CIDR x.x.x.x/x", 140 | "Default": "127.0.0.1/32", 141 | "Description": "CIDR Block for SSH access, default 127.0.0.1/32", 142 | "Type": "String" 143 | }, 144 | "Subnet": { 145 | "Description": "Subnet IDs", 146 | "Type": "AWS::EC2::Subnet::Id" 147 | }, 148 | "UsePublicIp": { 149 | "AllowedValues": [ 150 | "true", 151 | "false" 152 | ], 153 | "ConstraintDescription": "true/false", 154 | "Default": "true", 155 | "Description": "Should a public IP address be given to the instance", 156 | "Type": "String" 157 | }, 158 | "VpcId": { 159 | "Description": "VPC Id for this instance", 160 | "Type": "AWS::EC2::VPC::Id" 161 | } 162 | }, 163 | "Resources": { 164 | "InstanceWaitCondition": { 165 | "DependsOn": "MyInstance", 166 | "Properties": { 167 | "Handle": { 168 | "Ref": "InstanceWaitHandle" 169 | }, 170 | "Timeout": "1500" 171 | }, 172 | "Type": "AWS::CloudFormation::WaitCondition" 173 | }, 174 | "InstanceWaitHandle": { 175 | "Type": "AWS::CloudFormation::WaitConditionHandle" 176 | }, 177 | "MyInstance": { 178 | "Properties": { 179 | "IamInstanceProfile": { 180 | "Ref": "RootInstanceProfile" 181 | }, 182 | "ImageId": { 183 | "Fn::FindInMap": [ 184 | "AWSRegionAMI", 185 | { 186 | "Ref": "AWS::Region" 187 | }, 188 | { 189 | "Ref": "OperatingSystem" 190 | } 191 | ] 192 | }, 193 | "InstanceType": { 194 | "Ref": "MyInstanceType" 195 | }, 196 | "KeyName": { 197 | "Ref": "EC2KeyName" 198 | }, 199 | "NetworkInterfaces": [ 200 | { 201 | "AssociatePublicIpAddress": { 202 | "Ref": "UsePublicIp" 203 | }, 204 | "DeleteOnTermination": true, 205 | "DeviceIndex": "0", 206 | "GroupSet": { 207 | "Fn::If": [ 208 | "not_existing_sg", 209 | [ 210 | { 211 | "Ref": "SshSecurityGroup" 212 | } 213 | ], 214 | [ 215 | { 216 | "Ref": "SshSecurityGroup" 217 | }, 218 | { 219 | "Ref": "ExistingSecurityGroup" 220 | } 221 | ] 222 | ] 223 | }, 224 | "SubnetId": { 225 | "Ref": "Subnet" 226 | } 227 | } 228 | ], 229 | "UserData": { 230 | "Fn::Base64": { 231 | "Fn::Join": [ 232 | "", 233 | [ 234 | "#!/usr/bin/env bash\n", 235 | "\n", 236 | "set -x\n", 237 | "\n", 238 | "my_wait_handle=\"", 239 | { 240 | "Ref": "InstanceWaitHandle" 241 | }, 242 | "\"\n", 243 | "curl -X PUT -H 'Content-Type:' --data-binary '{ \"Status\" : \"SUCCESS\", \"Reason\" : \"Instance launched\", \"UniqueId\" : \"launch001\", \"Data\" : \"Instance launched.\"}' \"${my_wait_handle}\"", 244 | "\n", 245 | "\n" 246 | ] 247 | ] 248 | } 249 | } 250 | }, 251 | "Type": "AWS::EC2::Instance" 252 | }, 253 | "RootInstanceProfile": { 254 | "Properties": { 255 | "Roles": [ 256 | { 257 | "Ref": "RootRole" 258 | } 259 | ] 260 | }, 261 | "Type": "AWS::IAM::InstanceProfile" 262 | }, 263 | "RootRole": { 264 | "Properties": { 265 | "AssumeRolePolicyDocument": { 266 | "Statement": [ 267 | { 268 | "Action": [ 269 | "sts:AssumeRole" 270 | ], 271 | "Effect": "Allow", 272 | "Principal": { 273 | "Service": [ 274 | "ec2.amazonaws.com" 275 | ] 276 | } 277 | } 278 | ] 279 | } 280 | }, 281 | "Type": "AWS::IAM::Role" 282 | }, 283 | "SshSecurityGroup": { 284 | "Properties": { 285 | "GroupDescription": "SSH Secuirty group", 286 | "SecurityGroupIngress": [ 287 | { 288 | "CidrIp": { 289 | "Ref": "SshAccessCidr" 290 | }, 291 | "FromPort": "22", 292 | "IpProtocol": "tcp", 293 | "ToPort": "22" 294 | } 295 | ], 296 | "VpcId": { 297 | "Ref": "VpcId" 298 | } 299 | }, 300 | "Type": "AWS::EC2::SecurityGroup" 301 | } 302 | } 303 | } 304 | --------------------------------------------------------------------------------