├── tagger ├── __init__.py ├── cli.py └── tagger.py ├── setup.cfg ├── requirements.txt ├── setup.py ├── LICENSE ├── .gitignore └── README.md /tagger/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | --index-url https://pypi.python.org/simple/ 2 | 3 | -e . 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | import warnings 4 | 5 | setup( 6 | name='aws-tagger', 7 | version='0.6.3', 8 | packages=find_packages(), 9 | include_package_data=True, 10 | install_requires=[ 11 | 'boto3>=1.4.4', 12 | 'botocore>=1.5.7', 13 | 'click>=6.6', 14 | 'docutils>=0.13.1', 15 | 'futures>=3.0.5', 16 | 'jmespath>=0.9.1', 17 | 'retrying>=1.3.3', 18 | 's3transfer>=0.1.10', 19 | 'six>=1.10.0' 20 | ], 21 | entry_points={ 22 | "console_scripts": [ 23 | "aws-tagger=tagger.cli:cli", 24 | ] 25 | }, 26 | author="Patrick Cullen and the WaPo platform tools team", 27 | author_email="opensource@washingtonpost.com", 28 | url="https://github.com/washingtonpost/aws-tagger", 29 | download_url = "https://github.com/washingtonpost/aws-tagger/tarball/v0.6.3", 30 | keywords = ['tag', 'tagger', 'tagging', 'aws'], 31 | classifiers = [] 32 | ) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 The Washington Post 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | *.csv 91 | -------------------------------------------------------------------------------- /tagger/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import click 4 | import sys 5 | from tagger import MultipleResourceTagger, CSVResourceTagger 6 | from pprint import pprint 7 | 8 | @click.command() 9 | @click.option('--dryrun/--no-dryrun', default=False, help='Verbose output.') 10 | @click.option('--verbose/--no-verbose', default=False, help='Verbose output.') 11 | @click.option('--region', help='AWS region.') 12 | @click.option('--role', help='IAM role to use.') 13 | @click.option('--resource', multiple=True, help='Resource ID to tag.') 14 | @click.option('--tag', multiple=True, help='Tag to apply to resource in format "Key:Value".') 15 | @click.option('--csv', help='CSV file to read data from.') 16 | def cli(dryrun, verbose, region, role, resource, tag, csv): 17 | if csv and (len(resource) > 0 or len(tag) > 0): 18 | print("Cannot use --resource or --tag with --csv option") 19 | sys.exit(1) 20 | if csv: 21 | tagger = CSVResourceTagger(dryrun, verbose, role, region, tag_volumes=True) 22 | tagger.tag(csv) 23 | else: 24 | tagger = MultipleResourceTagger(dryrun, verbose, role, region, tag_volumes=True) 25 | tags = _tag_options_to_dict(tag) 26 | tagger.tag(resource, tags) 27 | 28 | def _tag_options_to_dict(tag_options): 29 | tags = {} 30 | for tag_option in tag_options: 31 | key, value = tag_option.split(':') 32 | tags[key] = value 33 | return tags 34 | 35 | if __name__ == '__main__': 36 | cli() 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws-tagger 2 | Tagging AWS resources is hard because each resource type has a different API which is slightly different. The AWS bulk tagging tool eliminates these differences so that you can simplify specify the resource ID and the tags and it takes care of the rest. Any tags that already exist on the resource will not be removed, but the values will be updated if the tag key already exists. Tags are case sensitive. 3 | 4 | ## Install 5 | ``` 6 | pip install aws-tagger 7 | ``` 8 | 9 | ## Usage 10 | 11 | ### Tag individual resource with a single tag 12 | ``` 13 | aws-tagger --resource i-07a9d0e5 --tag "App:Foobar" 14 | ``` 15 | 16 | ### Tag multiple resources with multiple tags 17 | ``` 18 | aws-tagger --resource i-07a9d0e5 --resource i-0456e3a9 --tag "App:Foobar" --tag "Team:My Team" 19 | ``` 20 | 21 | ### Tag multiple resources from a CSV file 22 | AWS Tagger can also take input from a CSV file. The column names of the CSV file are the tag keys and the colume values are the tag values. 23 | The resource id must be in a column called Id. To switch between regions, you can add a Region column with the standard AWS regions names like us-east-1. If the Region column is missing it assumes that the region is the same as the AWS credentials. 24 | ``` 25 | echo 'Id,Region,App' > my-resources.csv 26 | echo 'i-11111111,us-east-1,Foobar' >> my-resources.csv 27 | echo 'i-22222222,us-east-1,Foobar' >> my-resources.csv 28 | 29 | aws-tagger --csv my-resources.csv 30 | ``` 31 | 32 | ## AWS Resource Support 33 | AWS Tagger supports the following AWS resource types. 34 | 35 | ### EC2 instances 36 | Any EC2 volumes that are attached to the instance will be automatically tagged. 37 | ``` 38 | aws-tagger --resource i-07a9d0e5 --tag "App:Foobar" 39 | ``` 40 | 41 | ### S3 buckets 42 | ``` 43 | aws-tagger --resource my-bucket --tag "App:Foobar" 44 | ``` 45 | 46 | ### RDS instances 47 | ``` 48 | aws-tagger --resource arn:aws:rds:us-east-1:111111111:db:my-db --tag "App:Foobar" 49 | 50 | ``` 51 | 52 | ### EFS files systems 53 | ``` 54 | aws-tagger --resource arn:aws:elasticfilesystem:us-east-1:1111111111:file-system/fs-1111111 --tag "App:Foobar" 55 | ``` 56 | 57 | ### Elastic Load Balancers 58 | ``` 59 | aws-tagger --resource arn:aws:elasticloadbalancing:us-east-1:11111111111:loadbalancer/my-elb --tag "App:Foobar" 60 | ``` 61 | 62 | ### Application Load Balancers 63 | ``` 64 | aws-tagger --resource arn:aws:elasticloadbalancing:us-east-1:11111111111:loadbalancer/app/nile-content-api-syd-44c45100/f02ac6f33df89ba8 --tag "App:Foobar" 65 | ``` 66 | 67 | ### Elasticache clusters 68 | ``` 69 | aws-tagger --resource arn:aws:elasticache:us-east-1:111111111:cluster:my-cluster --tag "App:Foobar" 70 | ``` 71 | 72 | ### Elasticsearch clusters 73 | ``` 74 | aws-tagger --resource arn:aws:es:us-east-1:111111111:domain/my-domain --tag "App:Foobar" 75 | ``` 76 | 77 | ### Kinesis streams 78 | ``` 79 | aws-tagger --resource arn:aws:kinesis:us-east-1:111111111:stream/my-stream --tag "App:Foobar" 80 | ``` 81 | 82 | ### Cloudfront distributions 83 | ``` 84 | aws-tagger --resource arn:aws:cloudfront::1111111111:distribution/E1111111111111 --tag "App:Foobar" 85 | ``` 86 | 87 | ## AWS credentials 88 | AWS Tagger uses the standard AWS credential configuration options. 89 | 90 | ### Environment variables 91 | ``` 92 | export AWS_REGION="us-east-1" 93 | export AWS_ACCESS_KEY_ID="aka..." 94 | export AWS_SECRET_ACCESS_KEY="123..." 95 | aws-tagger --resource i-07a9d0e5 --tag "App:Foobar" 96 | ``` 97 | 98 | ### IAM Roles 99 | AWS Tagger also supports cross-account role assumption. You will still need to configure the initial AWS credentials using one of the methods above, but the role will be used to call the actuall AWS API. 100 | 101 | ``` 102 | aws-tagger --role arn:aws:iam::11111111111:role/MyRole --resource i-07a9d0e5 --tag "App:Foobar" 103 | 104 | -------------------------------------------------------------------------------- /tagger/tagger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import boto3 3 | import botocore 4 | from retrying import retry 5 | import socket 6 | import csv 7 | 8 | def _is_retryable_exception(exception): 9 | return not isinstance(exception, botocore.exceptions.ClientError) or \ 10 | (exception.response["Error"]["Code"] in ['LimitExceededException', 'RequestLimitExceeded', 'Throttling', 'ParamValidationError']) 11 | 12 | def _arn_to_name(resource_arn): 13 | # Example: arn:aws:elasticloadbalancing:us-east-1:397853141546:loadbalancer/pb-adn-arc2 14 | parts = resource_arn.split(':') 15 | name = parts[-1] 16 | parts = name.split('/', 1) 17 | if len(parts) == 2: 18 | name = parts[-1] 19 | 20 | return name 21 | 22 | def _format_dict(tags): 23 | output = [] 24 | for (key, value) in tags.items(): 25 | output.append("%s:%s" % (key, value)) 26 | 27 | return ", ".join(output) 28 | 29 | def _dict_to_aws_tags(tags): 30 | return [{'Key': key, 'Value': value} for (key, value) in tags.items() if not key.startswith('aws:')] 31 | 32 | def _aws_tags_to_dict(aws_tags): 33 | return {x['Key']: x['Value'] for x in aws_tags if not x['Key'].startswith('aws:')} 34 | 35 | def _fetch_temporary_credentials(role): 36 | sts = boto3.client('sts', region_name=os.environ.get('AWS_REGION', 'us-east-1')) 37 | 38 | response = sts.assume_role(RoleArn=role, RoleSessionName='aws-tagger.%s' % socket.gethostname()) 39 | access_key_id = response.get('Credentials', {}).get('AccessKeyId', None) 40 | secret_access_key = response.get('Credentials', {}).get('SecretAccessKey', None) 41 | session_token = response.get('Credentials', {}).get('SessionToken', None) 42 | return access_key_id, secret_access_key, session_token 43 | 44 | def _client(name, role, region): 45 | kwargs = {} 46 | 47 | if region: 48 | kwargs['region_name'] = region 49 | elif os.environ.get('AWS_REGION'): 50 | kwargs['region_name'] = os.environ['AWS_REGION'] 51 | 52 | if role: 53 | access_key_id, secret_access_key, session_token = _fetch_temporary_credentials(role) 54 | kwargs['aws_access_key_id'] = access_key_id 55 | kwargs['aws_secret_access_key'] = secret_access_key 56 | kwargs['aws_session_token'] = session_token 57 | 58 | return boto3.client(name, **kwargs) 59 | 60 | class SingleResourceTagger(object): 61 | def __init__(self, dryrun, verbose, role=None, region=None, tag_volumes=False): 62 | self.taggers = {} 63 | self.taggers['ec2'] = EC2Tagger(dryrun, verbose, role=role, region=region, tag_volumes=tag_volumes) 64 | self.taggers['elasticfilesystem'] = EFSTagger(dryrun, verbose, role=role, region=region) 65 | self.taggers['rds'] = RDSTagger(dryrun, verbose, role=role, region=region) 66 | self.taggers['elasticloadbalancing'] = LBTagger(dryrun, verbose, role=role, region=region) 67 | self.taggers['elasticache'] = ElasticacheTagger(dryrun, verbose, role=role, region=region) 68 | self.taggers['s3'] = S3Tagger(dryrun, verbose, role=role, region=region) 69 | self.taggers['es'] = ESTagger(dryrun, verbose, role=role, region=region) 70 | self.taggers['kinesis'] = KinesisTagger(dryrun, verbose, role=role, region=region) 71 | self.taggers['cloudfront'] = CloudfrontTagger(dryrun, verbose, role=role, region=region) 72 | self.taggers['logs'] = CloudWatchLogsTagger(dryrun, verbose, role=role, region=region) 73 | self.taggers['dynamodb'] = DynamoDBTagger(dryrun, verbose, role=role, region=region) 74 | self.taggers['lambda'] = LambdaTagger(dryrun, verbose, role=role, region=region) 75 | 76 | def tag(self, resource_id, tags): 77 | if resource_id == "": 78 | return 79 | 80 | if len(tags) == 0: 81 | return 82 | 83 | tagger = None 84 | resource_arn = resource_id 85 | if resource_id.startswith('arn:'): 86 | product, resource_id = self._parse_arn(resource_id) 87 | if product: 88 | tagger = self.taggers.get(product) 89 | else: 90 | tagger = self.taggers['s3'] 91 | 92 | 93 | if resource_id.startswith('i-'): 94 | tagger = self.taggers['ec2'] 95 | resource_arn = resource_id 96 | elif resource_id.startswith('vol-'): 97 | tagger = self.taggers['ec2'] 98 | resource_arn = resource_id 99 | elif resource_id.startswith('snap-'): 100 | tagger = self.taggers['ec2'] 101 | resource_arn = resource_id 102 | 103 | if tagger: 104 | tagger.tag(resource_arn, tags) 105 | else: 106 | print("Tagging is not support for this resource %s" % resource_id) 107 | 108 | def _parse_arn(self, resource_arn): 109 | product = None 110 | resource_id = None 111 | parts = resource_arn.split(':') 112 | if len(parts) > 5: 113 | product = parts[2] 114 | resource_id = parts[5] 115 | resource_parts = resource_id.split('/') 116 | if len(resource_parts) > 1: 117 | resource_id = resource_parts[-1] 118 | 119 | return product, resource_id 120 | 121 | class MultipleResourceTagger(object): 122 | def __init__(self, dryrun, verbose, role=None, region=None, tag_volumes=False): 123 | self.tagger = SingleResourceTagger(dryrun, verbose, role=role, region=region, tag_volumes=tag_volumes) 124 | 125 | def tag(self, resource_ids, tags): 126 | for resource_id in resource_ids: 127 | self.tagger.tag(resource_id, tags) 128 | 129 | class CSVResourceTagger(object): 130 | def __init__(self, dryrun, verbose, role=None, region=None, tag_volumes=False): 131 | self.dryrun = dryrun 132 | self.verbose = verbose 133 | self.tag_volumes = tag_volumes 134 | self.role = role 135 | self.region = region 136 | self.regional_tagger = {} 137 | self.resource_id_column = 'Id' 138 | self.region_column = 'Region' 139 | 140 | def tag(self, filename): 141 | with open(filename, 'rU') as csv_file: 142 | reader = csv.reader(csv_file) 143 | header_row = True 144 | tag_index = None 145 | 146 | for row in reader: 147 | if header_row: 148 | header_row = False 149 | tag_index = self._parse_header(row) 150 | else: 151 | self._tag_resource(tag_index, row) 152 | 153 | def _parse_header(self, header_row): 154 | tag_index = {} 155 | for index, name in enumerate(header_row): 156 | tag_index[name] = index 157 | 158 | return tag_index 159 | 160 | def _tag_resource(self, tag_index, row): 161 | resource_id = row[tag_index[self.resource_id_column]] 162 | tags = {} 163 | for (key, index) in tag_index.items(): 164 | value = row[index] 165 | if key != self.resource_id_column and key != self.region_column and value != "": 166 | tags[key] = value 167 | 168 | tagger = self._lookup_tagger(tag_index, row) 169 | tagger.tag(resource_id, tags) 170 | 171 | def _lookup_tagger(self, tag_index, row): 172 | region = self.region 173 | region_index = tag_index.get(self.region_column) 174 | 175 | if region_index is not None: 176 | region = row[region_index] 177 | if region == '': 178 | region = None 179 | 180 | tagger = self.regional_tagger.get(region) 181 | if tagger is None: 182 | tagger = SingleResourceTagger(self.dryrun, self.verbose, role=self.role, region=region, tag_volumes=self.tag_volumes) 183 | self.regional_tagger[region] = tagger 184 | 185 | return tagger 186 | 187 | class EC2Tagger(object): 188 | def __init__(self, dryrun, verbose, role=None, region=None, tag_volumes=False): 189 | self.dryrun = dryrun 190 | self.verbose = verbose 191 | self.ec2 = _client('ec2', role=role, region=region) 192 | self.volume_cache = {} 193 | if tag_volumes: 194 | self.add_volume_cache() 195 | 196 | def add_volume_cache(self): 197 | #TODO implement paging for describe instances 198 | reservations = self._ec2_describe_instances(MaxResults=1000) 199 | 200 | for reservation in reservations["Reservations"]: 201 | for instance in reservation["Instances"]: 202 | instance_id = instance['InstanceId'] 203 | volumes = instance.get('BlockDeviceMappings', []) 204 | self.volume_cache[instance_id] = [] 205 | for volume in volumes: 206 | ebs = volume.get('Ebs', {}) 207 | volume_id = ebs.get('VolumeId') 208 | if volume_id: 209 | self.volume_cache[instance_id].append(volume_id) 210 | 211 | def tag(self, instance_id, tags): 212 | aws_tags = _dict_to_aws_tags(tags) 213 | resource_ids = [instance_id] 214 | resource_ids.extend(self.volume_cache.get(instance_id, [])) 215 | if self.verbose: 216 | print("tagging %s with %s" % (", ".join(resource_ids), _format_dict(tags))) 217 | if not self.dryrun: 218 | try: 219 | self._ec2_create_tags(Resources=resource_ids, Tags=aws_tags) 220 | except botocore.exceptions.ClientError as exception: 221 | if exception.response["Error"]["Code"] in ['InvalidSnapshot.NotFound', 'InvalidVolume.NotFound', 'InvalidInstanceID.NotFound']: 222 | print("Resource not found: %s" % instance_id) 223 | else: 224 | raise exception 225 | 226 | 227 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 228 | def _ec2_describe_instances(self, **kwargs): 229 | return self.ec2.describe_instances(**kwargs) 230 | 231 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 232 | def _ec2_create_tags(self, **kwargs): 233 | return self.ec2.create_tags(**kwargs) 234 | 235 | class EFSTagger(object): 236 | def __init__(self, dryrun, verbose, role=None, region=None): 237 | self.dryrun = dryrun 238 | self.verbose = verbose 239 | self.efs = _client('efs', role=role, region=region) 240 | 241 | def tag(self, resource_arn, tags): 242 | file_system_id = _arn_to_name(resource_arn) 243 | aws_tags = _dict_to_aws_tags(tags) 244 | 245 | if self.verbose: 246 | print("tagging %s with %s" % (file_system_id, _format_dict(tags))) 247 | if not self.dryrun: 248 | try: 249 | self._efs_create_tags(FileSystemId=file_system_id, Tags=aws_tags) 250 | except botocore.exceptions.ClientError as exception: 251 | if exception.response["Error"]["Code"] in ['FileSystemNotFound']: 252 | print("Resource not found: %s" % resource_arn) 253 | else: 254 | raise exception 255 | 256 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 257 | def _efs_create_tags(self, **kwargs): 258 | return self.efs.create_tags(**kwargs) 259 | 260 | class DynamoDBTagger(object): 261 | def __init__(self, dryrun, verbose, role=None, region=None): 262 | self.dryrun = dryrun 263 | self.verbose = verbose 264 | self.dynamodb = _client('dynamodb', role=role, region=region) 265 | 266 | def tag(self, resource_arn, tags): 267 | aws_tags = _dict_to_aws_tags(tags) 268 | if self.verbose: 269 | print("tagging %s with %s" % (resource_arn, _format_dict(tags))) 270 | if not self.dryrun: 271 | try: 272 | self._dynamodb_tag_resource(ResourceArn=resource_arn, Tags=aws_tags) 273 | except botocore.exceptions.ClientError as exception: 274 | if exception.response["Error"]["Code"] in ['ResourceNotFoundException']: 275 | print("Resource not found: %s" % resource_arn) 276 | else: 277 | raise exception 278 | 279 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 280 | def _dynamodb_tag_resource(self, **kwargs): 281 | return self.dynamodb.tag_resource(**kwargs) 282 | 283 | 284 | class LambdaTagger(object): 285 | def __init__(self, dryrun, verbose, role=None, region=None): 286 | self.dryrun = dryrun 287 | self.verbose = verbose 288 | self.alambda = _client('lambda', role=role, region=region) 289 | 290 | def tag(self, resource_arn, tags): 291 | if self.verbose: 292 | print("tagging %s with %s" % (resource_arn, _format_dict(tags))) 293 | if not self.dryrun: 294 | try: 295 | self._lambda_tag_resource(Resource=resource_arn, Tags=tags) 296 | except botocore.exceptions.ClientError as exception: 297 | if exception.response["Error"]["Code"] in ['ResourceNotFoundException']: 298 | print("Resource not found: %s" % resource_arn) 299 | else: 300 | raise exception 301 | 302 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 303 | def _lambda_tag_resource(self, **kwargs): 304 | return self.alambda.tag_resource(**kwargs) 305 | 306 | 307 | class CloudWatchLogsTagger(object): 308 | def __init__(self, dryrun, verbose, role=None, region=None): 309 | self.dryrun = dryrun 310 | self.verbose = verbose 311 | self.logs= _client('logs', role=role, region=region) 312 | 313 | def tag(self, resource_arn, tags): 314 | if self.verbose: 315 | print("tagging %s with %s" % (resource_arn, _format_dict(tags))) 316 | log_group = None 317 | parts = resource_arn.split(':') 318 | if len(parts) > 0: 319 | log_group = parts[-1] 320 | 321 | if not log_group: 322 | print("Invalid ARN format for CloudWatch Logs: %s" % resource_arn) 323 | return 324 | 325 | if not self.dryrun: 326 | try: 327 | self._logs_tag_log_group(logGroupName=log_group, tags=tags) 328 | except botocore.exceptions.ClientError as exception: 329 | if exception.response["Error"]["Code"] in ['ResourceNotFoundException']: 330 | print("Resource not found: %s" % resource_arn) 331 | else: 332 | raise exception 333 | 334 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 335 | def _logs_tag_log_group(self, **kwargs): 336 | return self.logs.tag_log_group(**kwargs) 337 | 338 | 339 | class RDSTagger(object): 340 | def __init__(self, dryrun, verbose, role=None, region=None): 341 | self.dryrun = dryrun 342 | self.verbose = verbose 343 | self.rds = _client('rds', role=role, region=region) 344 | 345 | def tag(self, resource_arn, tags): 346 | aws_tags = _dict_to_aws_tags(tags) 347 | if self.verbose: 348 | print("tagging %s with %s" % (resource_arn, _format_dict(tags))) 349 | if not self.dryrun: 350 | try: 351 | self._rds_add_tags_to_resource(ResourceName=resource_arn, Tags=aws_tags) 352 | except botocore.exceptions.ClientError as exception: 353 | if exception.response["Error"]["Code"] in ['DBInstanceNotFound']: 354 | print("Resource not found: %s" % resource_arn) 355 | else: 356 | raise exception 357 | 358 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 359 | def _rds_add_tags_to_resource(self, **kwargs): 360 | return self.rds.add_tags_to_resource(**kwargs) 361 | 362 | class LBTagger(object): 363 | def __init__(self, dryrun, verbose, role=None, region=None): 364 | self.dryrun = dryrun 365 | self.verbose = verbose 366 | self.elb = _client('elb', role=role, region=region) 367 | self.alb = _client('elbv2', role=role, region=region) 368 | 369 | def tag(self, resource_arn, tags): 370 | aws_tags = _dict_to_aws_tags(tags) 371 | 372 | if self.verbose: 373 | print("tagging %s with %s" % (resource_arn, _format_dict(tags))) 374 | if not self.dryrun: 375 | try: 376 | if ':loadbalancer/app/' in resource_arn: 377 | self._alb_add_tags(ResourceArns=[resource_arn], Tags=aws_tags) 378 | else: 379 | elb_name = _arn_to_name(resource_arn) 380 | self._elb_add_tags(LoadBalancerNames=[elb_name], Tags=aws_tags) 381 | except botocore.exceptions.ClientError as exception: 382 | if exception.response["Error"]["Code"] in ['LoadBalancerNotFound']: 383 | print("Resource not found: %s" % resource_arn) 384 | else: 385 | raise exception 386 | 387 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 388 | def _elb_add_tags(self, **kwargs): 389 | return self.elb.add_tags(**kwargs) 390 | 391 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 392 | def _alb_add_tags(self, **kwargs): 393 | return self.alb.add_tags(**kwargs) 394 | 395 | class KinesisTagger(object): 396 | def __init__(self, dryrun, verbose, role=None, region=None): 397 | self.dryrun = dryrun 398 | self.verbose = verbose 399 | self.kinesis = _client('kinesis', role=role, region=region) 400 | 401 | def tag(self, resource_arn, tags): 402 | if self.verbose: 403 | print("tagging %s with %s" % (resource_arn, _format_dict(tags))) 404 | if not self.dryrun: 405 | try: 406 | stream_name = _arn_to_name(resource_arn) 407 | self._kinesis_add_tags_to_stream(StreamName=stream_name, Tags=tags) 408 | except botocore.exceptions.ClientError as exception: 409 | if exception.response["Error"]["Code"] in ['ResourceNotFoundException']: 410 | print("Resource not found: %s" % resource_arn) 411 | else: 412 | raise exception 413 | 414 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 415 | def _kinesis_add_tags_to_stream(self, **kwargs): 416 | return self.kinesis.add_tags_to_stream(**kwargs) 417 | 418 | class ESTagger(object): 419 | def __init__(self, dryrun, verbose, role=None, region=None): 420 | self.dryrun = dryrun 421 | self.verbose = verbose 422 | self.es = _client('es', role=role, region=region) 423 | 424 | def tag(self, resource_arn, tags): 425 | aws_tags = _dict_to_aws_tags(tags) 426 | if self.verbose: 427 | print("tagging %s with %s" % (resource_arn, _format_dict(tags))) 428 | if not self.dryrun: 429 | try: 430 | self._es_add_tags(ARN=resource_arn, TagList=aws_tags) 431 | except botocore.exceptions.ClientError as exception: 432 | if exception.response["Error"]["Code"] in ['ValidationException']: 433 | print("Resource not found: %s" % resource_arn) 434 | else: 435 | raise exception 436 | 437 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 438 | def _es_add_tags(self, **kwargs): 439 | return self.es.add_tags(**kwargs) 440 | 441 | class ElasticacheTagger(object): 442 | def __init__(self, dryrun, verbose, role=None, region=None): 443 | self.dryrun = dryrun 444 | self.verbose = verbose 445 | self.elasticache = _client('elasticache', role=role, region=region) 446 | 447 | def tag(self, resource_arn, tags): 448 | aws_tags = _dict_to_aws_tags(tags) 449 | if self.verbose: 450 | print("tagging %s with %s" % (resource_arn, _format_dict(tags))) 451 | if not self.dryrun: 452 | try: 453 | self._elasticache_add_tags_to_resource(ResourceName=resource_arn, Tags=aws_tags) 454 | except botocore.exceptions.ClientError as exception: 455 | if exception.response["Error"]["Code"] in ['CacheClusterNotFound']: 456 | print("Resource not found: %s" % resource_arn) 457 | else: 458 | raise exception 459 | 460 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 461 | def _elasticache_add_tags_to_resource(self, **kwargs): 462 | return self.elasticache.add_tags_to_resource(**kwargs) 463 | 464 | class CloudfrontTagger(object): 465 | def __init__(self, dryrun, verbose, role=None, region=None): 466 | self.dryrun = dryrun 467 | self.verbose = verbose 468 | self.cloudfront = _client('cloudfront', role=role, region=region) 469 | 470 | def tag(self, resource_arn, tags): 471 | aws_tags = _dict_to_aws_tags(tags) 472 | if self.verbose: 473 | print("tagging %s with %s" % (resource_arn, _format_dict(tags))) 474 | if not self.dryrun: 475 | try: 476 | self._cloudfront_tag_resource(Resource=resource_arn, Tags={'Items': aws_tags}) 477 | except botocore.exceptions.ClientError as exception: 478 | if exception.response["Error"]["Code"] in ['NoSuchResource']: 479 | print("Resource not found: %s" % resource_arn) 480 | else: 481 | raise exception 482 | 483 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 484 | def _cloudfront_tag_resource(self, **kwargs): 485 | return self.cloudfront.tag_resource(**kwargs) 486 | 487 | class S3Tagger(object): 488 | def __init__(self, dryrun, verbose, role=None, region=None): 489 | self.dryrun = dryrun 490 | self.verbose = verbose 491 | self.s3 = _client('s3', role=role, region=region) 492 | 493 | def tag(self, bucket_name, tags): 494 | try: 495 | if bucket_name.startswith('arn:'): 496 | bucket_name = _arn_to_name(bucket_name) 497 | response = self._s3_get_bucket_tagging(Bucket=bucket_name) 498 | # add existing tags 499 | for (key, value) in _aws_tags_to_dict(response.get('TagSet', [])).items(): 500 | if key not in tags: 501 | tags[key] = value 502 | except botocore.exceptions.ClientError as exception: 503 | if exception.response["Error"]["Code"] not in ['NoSuchTagSet', 'NoSuchBucket']: 504 | raise exception 505 | 506 | aws_tags = _dict_to_aws_tags(tags) 507 | if self.verbose: 508 | print("tagging %s with %s" % (bucket_name, _format_dict(tags))) 509 | if not self.dryrun: 510 | try: 511 | self._s3_put_bucket_tagging(Bucket=bucket_name, Tagging={'TagSet': aws_tags}) 512 | except botocore.exceptions.ClientError as exception: 513 | if exception.response["Error"]["Code"] in ['NoSuchBucket']: 514 | print("Resource not found: %s" % bucket_name) 515 | else: 516 | raise exception 517 | 518 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 519 | def _s3_get_bucket_tagging(self, **kwargs): 520 | return self.s3.get_bucket_tagging(**kwargs) 521 | 522 | @retry(retry_on_exception=_is_retryable_exception, stop_max_delay=30000, wait_exponential_multiplier=1000) 523 | def _s3_put_bucket_tagging(self, **kwargs): 524 | return self.s3.put_bucket_tagging(**kwargs) 525 | 526 | --------------------------------------------------------------------------------