├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── bin └── graffiti-monkey ├── conf ├── example_config.yml └── graffiti_monkey.yml ├── dev_requirements.txt ├── graffiti_monkey ├── __init__.py ├── cli.py ├── core.py └── exceptions.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py └── unit ├── __init__.py ├── test_exceptions.py └── test_init_unchanged.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | #bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | #lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | install: 5 | - pip install --use-mirrors -r requirements.txt 6 | - pip install --use-mirrors -r dev_requirements.txt 7 | - python setup.py develop 8 | script: nosetests tests/unit 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright 2013 Answers for AWS LLC 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt README.rst requirements.txt dev_requirements.txt 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Graffiti Monkey 2 | =============== 3 | 4 | .. image:: https://badges.gitter.im/Join%20Chat.svg 5 | :alt: Join the chat at https://gitter.im/Answers4AWS/graffiti-monkey 6 | :target: https://gitter.im/Answers4AWS/graffiti-monkey?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 7 | 8 | .. image:: https://travis-ci.org/Answers4AWS/graffiti-monkey.png?branch=master 9 | :target: https://travis-ci.org/Answers4AWS/graffiti-monkey 10 | :alt: Build Status 11 | 12 | The Graffiti Monkey goes around tagging things. By looking at the tags an EC2 13 | instance has, it copies those tags to the EBS Volumes that are attached to it, 14 | and then copies those tags to the EBS Snapshots. 15 | 16 | Usage 17 | ----- 18 | 19 | :: 20 | 21 | usage: graffiti-monkey [-h] [--region REGION] [--profile PROFILE] [--verbose] [--version] [--config CONFIG.YML] [--dryrun] 22 | 23 | Propagates tags from AWS EC2 instances to EBS volumes, and then to EBS 24 | snapshots. This makes it much easier to find things down the road. 25 | 26 | optional arguments: 27 | -h, --help show this help message and exit 28 | --region REGION the region to tag things in (default is current region of 29 | EC2 instance this is running on). E.g. us-east-1 30 | --profile PROFILE the profile to use to connect to EC2 (default is 'default', 31 | see Boto docs for profile credential options) 32 | --verbose, -v enable verbose output (-vvv for more) 33 | --version display version number and exit 34 | --config CONFIG.YML read a yaml configuration file. specify tags to propagate without changing code. 35 | --dryrun dryrun only, display tagging actions but do not perform them 36 | --append append propagated tags to existing tags (up to a total of ten tags). When not set, 37 | graffiti-monkey will overwrite existing tags. 38 | --volumes volume(s) to tag 39 | --snapshots snapshot(s) to tag 40 | --novolumes do not perform volume tagging 41 | --nosnapshots do not perform snapshot tagging 42 | 43 | Examples 44 | -------- 45 | 46 | Suppose you have the following in `us-east-1`: 47 | 48 | :: 49 | 50 | i-abcd1234 51 | - Tags: 52 | - Name: "Instance 1" 53 | 54 | vol-bcde3456 55 | - Attached to i-abcd1234 on /dev/sda1 56 | 57 | snap-cdef4567 58 | - Snapshot of vol-bcde3456 59 | 60 | 61 | When you run: 62 | 63 | :: 64 | 65 | graffiti-monkey --region us-east-1 66 | 67 | 68 | First, Graffiti Monkey will set the EBS volume tags 69 | 70 | :: 71 | 72 | vol-bcde3456 73 | - Tags: 74 | - Name: "Instance 1" 75 | - instance_id: i-abcd1234 76 | - device: /dev/sda1 77 | 78 | and then it will set the tags on the EBS Snapshot 79 | 80 | :: 81 | 82 | snap-cdef4567 83 | - Tags: 84 | - Name: "Instance 1" 85 | - instance_id: i-abcd1234 86 | - device: /dev/sda1 87 | 88 | 89 | 90 | Installation 91 | ------------ 92 | 93 | You can install Graffiti Monkey using the usual PyPI channels. Example: 94 | 95 | :: 96 | 97 | sudo pip install graffiti_monkey 98 | 99 | You can find the package details here: https://pypi.python.org/pypi/graffiti_monkey 100 | 101 | Alternatively, if you prefer to install from source: 102 | 103 | :: 104 | 105 | git clone git@github.com:Answers4AWS/graffiti-monkey.git 106 | cd graffiti-monkey 107 | python setup.py install 108 | 109 | 110 | Configuration 111 | ------------- 112 | 113 | This project uses `Boto `__ to 114 | call the AWS APIs. You can pass your AWS credentials to Boto can by using a 115 | :code:`.boto` file, IAM Roles or environment variables. Full information can be 116 | found here: 117 | 118 | http://boto.readthedocs.org/en/latest/boto_config_tut.html 119 | 120 | Graffiti-monkey itself can be configured using a yaml file 121 | 122 | :: 123 | 124 | --- 125 | #region: us-west-1 126 | _instance_tags_to_propagate: 127 | - 'Name' 128 | - 'Owner' 129 | 130 | _volume_tags_to_propagate: 131 | - 'Name' 132 | - 'instance_id' 133 | - 'device' 134 | - 'Owner' 135 | 136 | _volume_tags_to_be_set: 137 | - key: 'NU_ROLE' 138 | value: 'ebs' 139 | 140 | _snapshot_tags_to_be_set: 141 | - key: 'NU_ROLE' 142 | value: 'ebs_snapshot' 143 | 144 | _volumes_to_tag: 145 | # An empty list means tag all volumes 146 | # Example entries: 147 | # - 'vol-1ab2c345' 148 | # - 'vol-6de7f890' 149 | 150 | _snapshots_to_tag: 151 | # An empty list means tag all snapshots 152 | # Example entries: 153 | # - 'snap-12ab3c45' 154 | # - 'snap-6de7f890' 155 | 156 | :code:`_instance_tags_to_propagate` is used to define the tags that are propagated 157 | from an instance to its volumes. :code:`_volume_tags_to_propagate` defines the tags 158 | that are propagated from a volume to its snapshots. 159 | 160 | :code:`_volume_tags_to_be_set` is used to define the tags that are set on volumes 161 | by default. :code:`_snapshot_tags_to_be_set` defines the tags that are on snapshots 162 | by default. 163 | 164 | :code:`_volumes_to_tag` is used to define the volumes that are tagged. Leave empty 165 | to tag all volumes. :code:`_snapshots_to_tag` is used to define the snapshots to 166 | be tagged. Leave empty to tag all snapshots. 167 | 168 | If the configuration file is used, the _ entry headers must exist (those entries 169 | having no values or commented out values [as shown] is acceptable). 170 | 171 | When using yaml configuration files you need to have pyYAML. This can be easily setup 172 | using pip :code:`pip install PyYAML`. If you don't use config files you don't have 173 | this limitation. 174 | 175 | If options are specified in both the config file and on the command line, the config 176 | file options are used. 177 | 178 | 179 | Wiki 180 | ---- 181 | 182 | Can be found here: https://github.com/Answers4AWS/graffiti-monkey/wiki 183 | 184 | 185 | Source Code 186 | ----------- 187 | 188 | The Python source code for Graffiti Monkey is available on GitHub: 189 | 190 | https://github.com/Answers4AWS/graffiti-monkey 191 | 192 | 193 | About Answers for AWS 194 | --------------------- 195 | 196 | This code was written by `Peter 197 | Sankauskas `__, founder of `Answers for 198 | AWS `__ - a company focused on helping businesses 199 | learn how to use AWS, without doing it the hard way. If you are looking for help 200 | with AWS, please `contact us `__. 201 | 202 | 203 | License 204 | ------- 205 | 206 | Copyright 2013 Answers for AWS LLC 207 | 208 | Licensed under the Apache License, Version 2.0 (the "License"); you may 209 | not use this file except in compliance with the License. You may obtain 210 | a copy of the License at 211 | 212 | http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable 213 | law or agreed to in writing, software distributed under the License is 214 | distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 215 | KIND, either express or implied. See the License for the specific 216 | language governing permissions and limitations under the License. 217 | -------------------------------------------------------------------------------- /bin/graffiti-monkey: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright 2013 Answers for AWS LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | Graffiti Monkey 18 | ============== 19 | This script runs the Graffiti Monkey service. 20 | 21 | NOTE: It is here just as an easy way to run ./graffiti-monkey during 22 | development. This is not the script that is installed then you install graffiti 23 | monkey using pip. 24 | """ 25 | 26 | import sys, os 27 | sys.path.insert(0, os.path.abspath('..')) 28 | 29 | from graffiti_monkey import cli 30 | 31 | if __name__ == '__main__': 32 | cli.run() 33 | -------------------------------------------------------------------------------- /conf/example_config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | #region: us-west-1 3 | _instance_tags_to_propagate: 4 | - 'Name' 5 | 6 | _volume_tags_to_propagate: 7 | - 'Name' 8 | - 'instance_id' 9 | - 'device' 10 | 11 | _volume_tags_to_be_set: 12 | - key: 'role' 13 | value: 'ebs' 14 | 15 | _snapshot_tags_to_be_set: 16 | - key: 'role' 17 | value: 'ebs_snapshot' 18 | 19 | _instance_filter: 20 | # Filter values with the same key (eg, tag:app) are an OR 21 | # Different filter keys are an AND operation (eg, tag:app and tag:team) 22 | # See the Filter section in http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html#API_DescribeInstances_RequestParameters 23 | # Example entries: 24 | # 'tag:app': ['app1', 'app2'] 25 | # 'tag:team': 'team1' 26 | 27 | _volumes_to_tag: 28 | # An empty list means tag all volumes 29 | # Example entries: 30 | # - 'vol-1ab2c345' 31 | # - 'vol-6de7f890' 32 | 33 | _snapshots_to_tag: 34 | # An empty list means tag all snapshots 35 | # Example entries: 36 | # - 'snap-12ab3c45' 37 | # - 'snap-6de7f890' 38 | -------------------------------------------------------------------------------- /conf/graffiti_monkey.yml: -------------------------------------------------------------------------------- 1 | --- 2 | volume: 3 | collect: 4 | instance_id: attach_data.instance_id 5 | device: attach_data.device 6 | inspect: 7 | instance: 8 | type: instance 9 | id: instance_id 10 | collect: 11 | name: tags.Name 12 | tag: 13 | Name: name 14 | instance_id: instance_id 15 | device: device 16 | 17 | snapshot: 18 | collect: 19 | tag: 20 | 21 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | nose==1.3.0 2 | mock==1.0.1 3 | PyYAML==3.11 4 | moto==0.3.1 5 | -------------------------------------------------------------------------------- /graffiti_monkey/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Answers for AWS LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __author__ = 'Peter Sankauskas' 16 | __version__ = '1.0.0' 17 | -------------------------------------------------------------------------------- /graffiti_monkey/cli.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Answers for AWS LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import argparse 16 | import logging 17 | import sys 18 | 19 | from graffiti_monkey.core import GraffitiMonkey, Logging 20 | from graffiti_monkey import __version__ 21 | from graffiti_monkey.exceptions import GraffitiMonkeyException 22 | 23 | from boto.utils import get_instance_metadata 24 | 25 | 26 | __all__ = ('run', ) 27 | log = logging.getLogger(__name__) 28 | 29 | 30 | class GraffitiMonkeyCli(object): 31 | def __init__(self): 32 | self.region = None 33 | self.profile = None 34 | self.monkey = None 35 | self.args = None 36 | self.config = {"_instance_tags_to_propagate": ['Name'], 37 | "_volume_tags_to_propagate": ['Name', 'instance_id', 'device'], 38 | "_volume_tags_to_be_set": [], 39 | "_snapshot_tags_to_be_set": [], 40 | "_instance_filter": [], 41 | } 42 | self.dryrun = False 43 | self.append = False 44 | self.volumes = None 45 | self.snapshots = None 46 | self.instancefilter = None 47 | self.novolumes = False 48 | self.nosnapshots = False 49 | 50 | @staticmethod 51 | def _fail(message="Unknown failure", code=1): 52 | log.error(message) 53 | sys.exit(code) 54 | 55 | def get_argv(self): 56 | """ 57 | The parse_args method from ArgumentParser expects to not get the script title when arguments are passed to the 58 | method. So the first element is omitted. 59 | """ 60 | return sys.argv[1:] 61 | 62 | def set_cli_args(self): 63 | parser = argparse.ArgumentParser(description='Propagates tags from AWS EC2 instances to EBS volumes, and then to EBS snapshots. This makes it much easier to find things down the road.') 64 | parser.add_argument('--region', metavar='REGION', 65 | help='the region to tag things in (default is current region of EC2 instance this is running on). E.g. us-east-1') 66 | parser.add_argument('--profile', metavar='PROFILE', 67 | help='the profile (credentials) to use to connect to EC2') 68 | parser.add_argument('--verbose', '-v', action='count', 69 | help='enable verbose output (-vvv for more)') 70 | parser.add_argument('--version', action='version', version='%(prog)s ' + __version__, 71 | help='display version number and exit') 72 | parser.add_argument('--config', '-c', nargs="?", type=argparse.FileType('r'), 73 | default=None, help="Give a yaml configuration file") 74 | parser.add_argument('--dryrun', action='store_true', 75 | help='dryrun only, display tagging actions but do not perform them') 76 | parser.add_argument('--append', action='store_true', 77 | help='append propagated tags to existing tags (up to a total of ten tags)') 78 | parser.add_argument('--volumes', action='append', 79 | help='volume-ids to tag') 80 | parser.add_argument('--snapshots', action='append', 81 | help='snapshot-ids to tag'), 82 | parser.add_argument('--novolumes', action='store_true', 83 | help='do not perform volume tagging') 84 | parser.add_argument('--nosnapshots', action='store_true', 85 | help='do not perform snapshot tagging') 86 | self.args = parser.parse_args(self.get_argv()) 87 | 88 | @staticmethod 89 | def fail_due_to_bad_config_file(self): 90 | self._fail("Something went wrong reading the passed yaml config file. " 91 | "Make sure to use valid yaml syntax. " 92 | "Also the start of the file should not be marked with '---'.", 6) 93 | 94 | def set_config(self): 95 | if self.args.config: 96 | try: 97 | import yaml 98 | except: 99 | log.error("When the config parameter is used, you need to have the python PyYAML library.") 100 | log.error("It can be installed with pip `pip install PyYAML`.") 101 | sys.exit(5) 102 | 103 | try: 104 | #TODO: take default values and these can be overwritten by config 105 | self.config = yaml.load(self.args.config) 106 | if self.config is None: 107 | self.fail_due_to_bad_config_file() 108 | except: 109 | self.fail_due_to_bad_config_file() 110 | 111 | 112 | 113 | def set_region(self): 114 | if self.args.region: 115 | self.region = self.args.region 116 | elif "region" in self.config.keys(): 117 | self.region = self.config["region"] 118 | else: 119 | # If no region was specified, assume this is running on an EC2 instance 120 | # and work out what region it is in 121 | log.debug("Figure out which region I am running in...") 122 | instance_metadata = get_instance_metadata(timeout=5) 123 | log.debug('Instance meta-data: %s', instance_metadata) 124 | if not instance_metadata: 125 | GraffitiMonkeyCli._fail('Could not determine region. This script is either not running on an EC2 instance (in which case you should use the --region option), or the meta-data service is down') 126 | 127 | self.region = instance_metadata['placement']['availability-zone'][:-1] 128 | log.debug("Running in region: %s", self.region) 129 | 130 | def set_profile(self): 131 | if self.args.profile: 132 | self.profile = self.args.profile 133 | elif "profile" in self.config.keys(): 134 | self.profile = self.config["profile"] 135 | else: 136 | self.profile = 'default' 137 | log.debug("Using profile: %s", self.profile) 138 | 139 | def set_dryrun(self): 140 | self.dryrun = self.args.dryrun 141 | 142 | def set_append(self): 143 | self.append = self.args.append 144 | 145 | def set_volumes(self): 146 | if self.args.volumes: 147 | self.volumes = self.args.volumes 148 | elif "_volumes_to_tag" in self.config.keys(): 149 | self.volumes = self.config["_volumes_to_tag"] 150 | 151 | def set_snapshots(self): 152 | if self.args.snapshots: 153 | self.snapshots = self.args.snapshots 154 | elif "_snapshots_to_tag" in self.config.keys(): 155 | self.snapshots = self.config["_snapshots_to_tag"] 156 | 157 | def set_instancefilter(self): 158 | if "_instance_filter" in self.config.keys(): 159 | self.instancefilter = self.config["_instance_filter"] 160 | 161 | def set_novolumes(self): 162 | self.novolumes = self.args.novolumes 163 | 164 | def set_nosnapshots(self): 165 | self.nosnapshots = self.args.nosnapshots 166 | 167 | def config_default(self, key): 168 | default_value = list() 169 | value = self.config.get(key) 170 | return value if value is not None else default_value 171 | 172 | def initialize_monkey(self): 173 | self.monkey = GraffitiMonkey(self.region, 174 | self.profile, 175 | self.config["_instance_tags_to_propagate"], 176 | self.config["_volume_tags_to_propagate"], 177 | self.config_default("_volume_tags_to_be_set"), 178 | self.config_default("_snapshot_tags_to_be_set"), 179 | self.dryrun, 180 | self.append, 181 | self.volumes, 182 | self.snapshots, 183 | self.instancefilter, 184 | self.novolumes, 185 | self.nosnapshots 186 | ) 187 | 188 | def start_tags_propagation(self): 189 | self.monkey.propagate_tags() 190 | 191 | def exit_succesfully(self): 192 | log.info('Graffiti Monkey completed successfully!') 193 | sys.exit(0) 194 | 195 | def run(self): 196 | self.set_cli_args() 197 | 198 | Logging().configure(self.args.verbose) 199 | log.debug("CLI parse args: %s", self.args) 200 | 201 | self.set_config() 202 | self.set_region() 203 | self.set_profile() 204 | self.set_dryrun() 205 | self.set_append() 206 | self.set_volumes() 207 | self.set_snapshots() 208 | self.set_instancefilter() 209 | self.set_novolumes() 210 | self.set_nosnapshots() 211 | 212 | try: 213 | self.initialize_monkey() 214 | self.start_tags_propagation() 215 | 216 | except GraffitiMonkeyException as e: 217 | GraffitiMonkeyCli._fail(e.message) 218 | 219 | self.exit_succesfully() 220 | 221 | 222 | def run(): 223 | cli = GraffitiMonkeyCli() 224 | cli.run() 225 | -------------------------------------------------------------------------------- /graffiti_monkey/core.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Answers for AWS LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | 17 | from exceptions import * 18 | 19 | import boto 20 | from boto import ec2 21 | 22 | import time 23 | 24 | __all__ = ('GraffitiMonkey', 'Logging') 25 | log = logging.getLogger(__name__) 26 | 27 | 28 | class GraffitiMonkey(object): 29 | def __init__(self, region, profile, instance_tags_to_propagate, volume_tags_to_propagate, volume_tags_to_be_set, snapshot_tags_to_be_set, dryrun, append, volumes_to_tag, snapshots_to_tag, instance_filter, novolumes, nosnapshots): 30 | # This list of tags associated with an EC2 instance to propagate to 31 | # attached EBS volumes 32 | self._instance_tags_to_propagate = instance_tags_to_propagate 33 | 34 | # This is a list of tags associated with a volume to propagate to 35 | # a snapshot created from the volume 36 | self._volume_tags_to_propagate = volume_tags_to_propagate 37 | 38 | # This is a dict of tags (keys and values) which will be set on the volumes (ebs) 39 | self._volume_tags_to_be_set = volume_tags_to_be_set 40 | 41 | # This is a dict of tags (keys and values) which will be set on the snapshots 42 | self._snapshot_tags_to_be_set = snapshot_tags_to_be_set 43 | 44 | # The region to operate in 45 | self._region = region 46 | 47 | # The profile to use 48 | self._profile = profile 49 | 50 | # Whether this is a dryrun 51 | self._dryrun = dryrun 52 | 53 | # If we are appending tags 54 | self._append = append 55 | 56 | # Volumes we will tag 57 | self._volumes_to_tag = volumes_to_tag 58 | 59 | # Snapshots we will tag 60 | self._snapshots_to_tag = snapshots_to_tag 61 | 62 | # Filter instances by a given param and propagate their tags to their attached volumes 63 | self._instance_filter = instance_filter 64 | 65 | # If we process volumes 66 | self._novolumes = novolumes 67 | 68 | # If we process snapshots 69 | self._nosnapshots = nosnapshots 70 | 71 | log.info("Starting Graffiti Monkey") 72 | log.info("Options: dryrun %s, append %s, novolumes %s, nosnapshots %s", self._dryrun, self._append, self._novolumes, self._nosnapshots) 73 | log.info("Connecting to region %s using profile %s", self._region, self._profile) 74 | try: 75 | self._conn = ec2.connect_to_region(self._region, profile_name=self._profile) 76 | except boto.exception.NoAuthHandlerFound: 77 | raise GraffitiMonkeyException('No AWS credentials found - check your credentials') 78 | except boto.provider.ProfileNotFoundError: 79 | log.info("Connecting to region %s using default credentials", self._region) 80 | try: 81 | self._conn = ec2.connect_to_region(self._region) 82 | except boto.exception.NoAuthHandlerFound: 83 | raise GraffitiMonkeyException('No AWS credentials found - check your credentials') 84 | 85 | 86 | def propagate_tags(self): 87 | ''' Propagates tags by copying them from EC2 instance to EBS volume, and 88 | then to snapshot ''' 89 | 90 | volumes = [] 91 | if not self._novolumes: 92 | volumes = self.tag_volumes() 93 | 94 | volumes = { v.id: v for v in volumes } 95 | 96 | if not self._nosnapshots: 97 | self.tag_snapshots(volumes) 98 | 99 | def tag_volumes(self): 100 | ''' Gets a list of volumes, and then loops through them tagging 101 | them ''' 102 | 103 | storage_counter = 0 104 | volumes = [] 105 | instances = {} 106 | 107 | if self._volumes_to_tag: 108 | log.info('Using volume list from cli/config file') 109 | 110 | # Max of 200 filters in a request 111 | for chunk in (self._volumes_to_tag[n:n+200] for n in xrange(0, len(self._volumes_to_tag), 200)): 112 | chunk_volumes = self._conn.get_all_volumes( 113 | filters = { 'volume-id': chunk } 114 | ) 115 | volumes += chunk_volumes 116 | 117 | chunk_instance_ids = set(v.attach_data.instance_id for v in chunk_volumes) 118 | reservations = self._conn.get_all_instances( 119 | filters = {'instance-id': [id for id in chunk_instance_ids]} 120 | ) 121 | for reservation in reservations: 122 | for instance in reservation.instances: 123 | instances[instance.id] = instance 124 | 125 | volume_ids = [v.id for v in volumes] 126 | 127 | ''' We can't trust the volume list from the config file so we 128 | test the status of each volume and remove any that raise an exception ''' 129 | for volume_id in self._volumes_to_tag: 130 | if volume_id not in volume_ids: 131 | log.info('Volume %s does not exist and will not be tagged', volume_id) 132 | self._volumes_to_tag.remove(volume_id) 133 | 134 | elif self._instance_filter: 135 | log.info('Filter instances and retrieve volume ids') 136 | instances = dict((instance.id, instance) for instance in self._conn.get_only_instances(filters=self._instance_filter)) 137 | volumes = self._conn.get_all_volumes(filters={'attachment.instance-id': list(instances.keys())}) 138 | 139 | else: 140 | log.info('Getting list of all volumes') 141 | volumes = self._conn.get_all_volumes() 142 | reservations = self._conn.get_all_instances() 143 | for reservation in reservations: 144 | for instance in reservation.instances: 145 | instances[instance.id] = instance 146 | 147 | if not volumes: 148 | log.info('No volumes found') 149 | return True 150 | 151 | log.debug('Volume list >%s<', volumes) 152 | total_vols = len(volumes) 153 | log.info('Found %d volume(s)', total_vols) 154 | this_vol = 0 155 | for volume in volumes: 156 | this_vol += 1 157 | storage_counter += volume.size 158 | log.info ('Processing volume %d of %d total volumes', this_vol, total_vols) 159 | 160 | if volume.status != 'in-use': 161 | log.debug('Skipping %s as it is not attached to an EC2 instance, so there is nothing to propagate', volume.id) 162 | continue 163 | 164 | for attempt in range(5): 165 | try: 166 | self.tag_volume(volume, instances) 167 | except boto.exception.EC2ResponseError, e: 168 | log.error("Encountered Error %s on volume %s", e.error_code, volume.id) 169 | break 170 | except boto.exception.BotoServerError, e: 171 | log.error("Encountered Error %s on volume %s, waiting %d seconds then retrying", e.error_code, volume.id, attempt) 172 | time.sleep(attempt) 173 | else: 174 | break 175 | else: 176 | log.error("Encountered Error %s on volume %s, %d retries failed, continuing", e.error_code, volume.id, attempt) 177 | continue 178 | 179 | log.info('Processed a total of {0} GB of AWS Volumes'.format(storage_counter)) 180 | log.info('Completed processing all volumes') 181 | 182 | return volumes 183 | 184 | 185 | def tag_volume(self, volume, instances): 186 | ''' Tags a specific volume ''' 187 | 188 | instance_id = None 189 | if volume.attach_data.instance_id: 190 | instance_id = volume.attach_data.instance_id 191 | device = None 192 | if volume.attach_data.device: 193 | device = volume.attach_data.device 194 | 195 | instance_tags = instances[instance_id].tags 196 | 197 | tags_to_set = {} 198 | if self._append: 199 | tags_to_set = volume.tags 200 | for tag_name in self._instance_tags_to_propagate: 201 | log.debug('Trying to propagate instance tag: %s', tag_name) 202 | if tag_name in instance_tags: 203 | value = instance_tags[tag_name] 204 | tags_to_set[tag_name] = value 205 | 206 | # Additional tags 207 | tags_to_set['instance_id'] = instance_id 208 | tags_to_set['device'] = device 209 | 210 | # Set default tags for volume 211 | for tag in self._volume_tags_to_be_set: 212 | log.debug('Trying to set default tag: %s=%s', tag['key'], tag['value']) 213 | tags_to_set[tag['key']] = tag['value'] 214 | 215 | if self._dryrun: 216 | log.info('DRYRUN: Volume %s would have been tagged %s', volume.id, tags_to_set) 217 | else: 218 | self._set_resource_tags(volume, tags_to_set) 219 | return True 220 | 221 | 222 | def tag_snapshots(self, volumes): 223 | ''' Gets a list of snapshots, and then loops through them tagging 224 | them ''' 225 | 226 | snapshots = [] 227 | if self._snapshots_to_tag: 228 | log.info('Using snapshot list from cli/config file') 229 | 230 | # Max of 200 filters in a request 231 | for chunk in (self._snapshots_to_tag[n:n+200] for n in xrange(0, len(self._snapshots_to_tag), 200)): 232 | chunk_snapshots = self._conn.get_all_snapshots( 233 | filters = { 'snapshot-id': chunk } 234 | ) 235 | snapshots += chunk_snapshots 236 | snapshot_ids = [s.id for s in snapshots] 237 | 238 | ''' We can't trust the snapshot list from the config file so we 239 | test the status of each and remove any that raise an exception ''' 240 | for snapshot_id in self._snapshots_to_tag: 241 | if snapshot_id not in snapshot_ids: 242 | log.info('Snapshot %s does not exist and will not be tagged', snapshot_id) 243 | self._snapshots_to_tag.remove(snapshot) 244 | else: 245 | log.info('Getting list of all snapshots') 246 | snapshots = self._conn.get_all_snapshots(owner='self') 247 | 248 | if not snapshots: 249 | log.info('No snapshots found') 250 | return True 251 | 252 | all_volume_ids = set(s.volume_id for s in snapshots) 253 | extra_volume_ids = [id for id in all_volume_ids if id not in volumes] 254 | 255 | ''' Fetch any extra volumes that weren't carried over from tag_volumes() (if any) ''' 256 | for chunk in (extra_volume_ids[n:n+200] for n in xrange(0, len(extra_volume_ids), 200)): 257 | extra_volumes = self._conn.get_all_volumes( 258 | filters = { 'volume-id': chunk } 259 | ) 260 | for vol in extra_volumes: 261 | volumes[vol.id] = vol 262 | 263 | log.debug('Snapshot list >%s<', snapshots) 264 | total_snaps = len(snapshots) 265 | log.info('Found %d snapshot(s)', total_snaps) 266 | this_snap = 0 267 | 268 | for snapshot in snapshots: 269 | this_snap += 1 270 | log.info ('Processing snapshot %d of %d total snapshots', this_snap, total_snaps) 271 | for attempt in range(5): 272 | try: 273 | self.tag_snapshot(snapshot, volumes) 274 | except boto.exception.EC2ResponseError, e: 275 | log.error("Encountered Error %s on snapshot %s", e.error_code, snapshot.id) 276 | break 277 | except boto.exception.BotoServerError, e: 278 | log.error("Encountered Error %s on snapshot %s, waiting %d seconds then retrying", e.error_code, snapshot.id, attempt) 279 | time.sleep(attempt) 280 | else: 281 | break 282 | else: 283 | log.error("Encountered Error %s on snapshot %s, %d retries failed, continuing", e.error_code, snapshot.id, attempt) 284 | continue 285 | log.info('Completed processing all snapshots') 286 | 287 | def tag_snapshot(self, snapshot, volumes): 288 | ''' Tags a specific snapshot ''' 289 | 290 | volume_id = snapshot.volume_id 291 | 292 | if volume_id not in volumes: 293 | log.info("Snapshot %s volume %s not found. Snapshot will not be tagged", snapshot.id, volume_id) 294 | return 295 | 296 | volume_tags = volumes[volume_id].tags 297 | 298 | tags_to_set = {} 299 | if self._append: 300 | tags_to_set = snapshot.tags 301 | for tag_name in self._volume_tags_to_propagate: 302 | log.debug('Trying to propagate volume tag: %s', tag_name) 303 | if tag_name in volume_tags: 304 | tags_to_set[tag_name] = volume_tags[tag_name] 305 | 306 | # Set default tags for snapshot 307 | for tag in self._snapshot_tags_to_be_set: 308 | log.debug('Trying to set default tag: %s=%s', tag['key'], tag['value']) 309 | tags_to_set[tag['key']] = tag['value'] 310 | 311 | if self._dryrun: 312 | log.info('DRYRUN: Snapshot %s would have been tagged %s', snapshot.id, tags_to_set) 313 | else: 314 | self._set_resource_tags(snapshot, tags_to_set) 315 | return True 316 | 317 | 318 | def _set_resource_tags(self, resource, tags): 319 | ''' Sets the tags on the given AWS resource ''' 320 | 321 | if not isinstance(resource, ec2.ec2object.TaggedEC2Object): 322 | msg = 'Resource %s is not an instance of TaggedEC2Object' % resource 323 | raise GraffitiMonkeyException(msg) 324 | 325 | delta_tags = {} 326 | 327 | for tag_key, tag_value in tags.iteritems(): 328 | if not tag_key in resource.tags or resource.tags[tag_key] != tag_value: 329 | delta_tags[tag_key] = tag_value 330 | 331 | if len(delta_tags) == 0: 332 | return 333 | 334 | log.info('Tagging %s with [%s]', resource.id, delta_tags) 335 | resource.add_tags(delta_tags) 336 | 337 | 338 | 339 | class Logging(object): 340 | # Logging formats 341 | _log_simple_format = '%(asctime)s [%(levelname)s] %(message)s' 342 | _log_detailed_format = '%(asctime)s [%(levelname)s] [%(name)s(%(lineno)s):%(funcName)s] %(message)s' 343 | 344 | def configure(self, verbosity = None): 345 | ''' Configure the logging format and verbosity ''' 346 | 347 | # Configure our logging output 348 | if verbosity >= 2: 349 | logging.basicConfig(level=logging.DEBUG, format=self._log_detailed_format, datefmt='%Y-%m-%d %H:%M:%S') 350 | elif verbosity >= 1: 351 | logging.basicConfig(level=logging.INFO, format=self._log_detailed_format, datefmt='%Y-%m-%d %H:%M:%S') 352 | else: 353 | logging.basicConfig(level=logging.INFO, format=self._log_simple_format, datefmt='%Y-%m-%d %H:%M:%S') 354 | 355 | # Configure Boto's logging output 356 | if verbosity >= 4: 357 | logging.getLogger('boto').setLevel(logging.DEBUG) 358 | elif verbosity >= 3: 359 | logging.getLogger('boto').setLevel(logging.INFO) 360 | else: 361 | logging.getLogger('boto').setLevel(logging.CRITICAL) 362 | -------------------------------------------------------------------------------- /graffiti_monkey/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Answers for AWS LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | class GraffitiMonkeyException(Exception): 16 | ''' Base Graffiti Monkey Exception ''' 17 | pass 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto>=2.7 2 | # Following is only needed when using a yaml config file 3 | PyYAML>=3.11 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2013 Answers for AWS LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | setuptools install script for Graffiti Monkey 18 | """ 19 | 20 | import sys 21 | major, minor = sys.version_info[0:2] 22 | if major != 2 or minor < 7: 23 | print 'Graffiti Monkey requires Python 2.7.x' 24 | sys.exit(1) 25 | 26 | from setuptools import setup, find_packages 27 | 28 | import graffiti_monkey 29 | 30 | with open('requirements.txt') as fh: 31 | requires = [requirement.strip() for requirement in fh] 32 | 33 | entry_points = { 34 | 'console_scripts': [ 35 | 'graffiti-monkey = graffiti_monkey.cli:run', 36 | ] 37 | } 38 | 39 | exclude_packages = [ 40 | 'tests', 41 | 'tests.*', 42 | ] 43 | 44 | setup( 45 | name='graffiti_monkey', 46 | version=graffiti_monkey.__version__, 47 | description='A service that propagates tags from EC2 instances to EBS Volumes, and then to EBS Snapshots', 48 | long_description=open('README.rst').read(), 49 | author=graffiti_monkey.__author__, 50 | author_email='info@answersforaws.com', 51 | url='https://github.com/Answers4AWS/graffiti-monkey', 52 | packages=find_packages(exclude=exclude_packages), 53 | package_dir={'graffiti_monkey': 'graffiti_monkey'}, 54 | include_package_data=True, 55 | zip_safe=False, 56 | install_requires=requires, 57 | entry_points=entry_points, 58 | license=open("LICENSE.txt").read(), 59 | classifiers=( 60 | 'Development Status :: 4 - Beta', 61 | 'Environment :: Console', 62 | 'Intended Audience :: Developers', 63 | 'Intended Audience :: Information Technology', 64 | 'Intended Audience :: System Administrators', 65 | 'License :: OSI Approved :: Apache Software License', 66 | 'Natural Language :: English', 67 | 'Programming Language :: Python :: 2.7', 68 | 'Topic :: System :: Installation/Setup', 69 | 'Topic :: Utilities', 70 | ) 71 | ) 72 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Answers for AWS LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Answers for AWS LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /tests/unit/test_exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Answers for AWS LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import unittest 16 | 17 | from graffiti_monkey.exceptions import * 18 | 19 | class ExceptionTests(unittest.TestCase): 20 | def test_new_exception(self): 21 | e = GraffitiMonkeyException() 22 | self.assertIsInstance(e, GraffitiMonkeyException) 23 | 24 | def raise_GraffitiMonkeyException(self): 25 | raise GraffitiMonkeyException('msg') 26 | 27 | def test_raise_GraffitiMonkeyException(self): 28 | self.assertRaises(GraffitiMonkeyException, self.raise_GraffitiMonkeyException) 29 | -------------------------------------------------------------------------------- /tests/unit/test_init_unchanged.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | import unittest 15 | import mock 16 | from moto import mock_ec2 17 | 18 | """ 19 | These tests are put in place as to assure that behavior is not changed due to changes in the constructor of 20 | GraffitiMonkey and . 21 | """ 22 | from graffiti_monkey.cli import GraffitiMonkeyCli 23 | 24 | 25 | class InitUnchangedTests(unittest.TestCase): 26 | 27 | @staticmethod 28 | def do_not_propagate_tags_nor_exit(graffiti_monkey_cli): 29 | mock_do_nothing = mock.Mock() 30 | graffiti_monkey_cli.start_tags_propagation = mock_do_nothing 31 | graffiti_monkey_cli.exit_succesfully = mock_do_nothing 32 | 33 | @staticmethod 34 | def set_cli_arguments(graffiti_monkey_cli): 35 | mock_get_cli_arguments = mock.Mock() 36 | mock_get_cli_arguments.return_value = ['-v', '--region', 'us-west-1'] 37 | graffiti_monkey_cli.get_argv = mock_get_cli_arguments 38 | 39 | @staticmethod 40 | def set_cli_arguments_with_default_config_file(graffiti_monkey_cli): 41 | mock_get_cli_arguments = mock.Mock() 42 | mock_get_cli_arguments.return_value = ['-v', '--region', 'us-west-1', '--config', 'conf/example_config.yml'] 43 | graffiti_monkey_cli.get_argv = mock_get_cli_arguments 44 | 45 | @mock_ec2 46 | def test_graffiti_monkey_instance_tags_to_propagate_should_be_the_same(self): 47 | cli = GraffitiMonkeyCli() 48 | self.set_cli_arguments(cli) 49 | self.do_not_propagate_tags_nor_exit(cli) 50 | cli.run() 51 | self.assertEquals(cli.monkey._instance_tags_to_propagate, ['Name']) 52 | 53 | @mock_ec2 54 | def test_graffiti_monkey_volume_tags_to_propagate_should_be_the_same(self): 55 | cli = GraffitiMonkeyCli() 56 | self.set_cli_arguments(cli) 57 | self.do_not_propagate_tags_nor_exit(cli) 58 | cli.run() 59 | self.assertEquals(cli.monkey._volume_tags_to_propagate, ['Name', 'instance_id', 'device']) 60 | 61 | @mock_ec2 62 | def test_region_unchanged(self): 63 | cli = GraffitiMonkeyCli() 64 | self.set_cli_arguments(cli) 65 | self.do_not_propagate_tags_nor_exit(cli) 66 | cli.run() 67 | self.assertEquals(cli.monkey._region, "us-west-1") 68 | 69 | @mock_ec2 70 | def test_graffiti_monkey_instance_tags_to_propagate_should_be_the_same_with_default_config(self): 71 | cli = GraffitiMonkeyCli() 72 | self.set_cli_arguments_with_default_config_file(cli) 73 | self.do_not_propagate_tags_nor_exit(cli) 74 | cli.run() 75 | self.assertEquals(cli.monkey._instance_tags_to_propagate, ['Name']) 76 | 77 | @mock_ec2 78 | def test_graffiti_monkey_volume_tags_to_propagate_should_be_the_same_with_default_config(self): 79 | cli = GraffitiMonkeyCli() 80 | self.set_cli_arguments_with_default_config_file(cli) 81 | self.do_not_propagate_tags_nor_exit(cli) 82 | cli.run() 83 | self.assertEquals(cli.monkey._volume_tags_to_propagate, ['Name', 'instance_id', 'device']) 84 | 85 | @mock_ec2 86 | def test_region_unchanged_with_default_config(self): 87 | cli = GraffitiMonkeyCli() 88 | self.set_cli_arguments_with_default_config_file(cli) 89 | self.do_not_propagate_tags_nor_exit(cli) 90 | cli.run() 91 | self.assertEquals(cli.monkey._region, "us-west-1") 92 | --------------------------------------------------------------------------------