diff --git a/skew/resources/__init__.py b/skew/resources/__init__.py index 7906b2c..ec57ab2 100644 --- a/skew/resources/__init__.py +++ b/skew/resources/__init__.py @@ -17,11 +17,13 @@ # Maps resources names as they appear in ARN's to the path name # of the Python class representing that resource. ResourceTypes = { + 'aws.acm.certificate': 'aws.acm.Certificate', 'aws.apigateway.restapis': 'aws.apigateway.RestAPI', 'aws.autoscaling.autoScalingGroup': 'aws.autoscaling.AutoScalingGroup', 'aws.autoscaling.launchConfigurationName': 'aws.autoscaling.LaunchConfiguration', 'aws.cloudfront.distribution': 'aws.cloudfront.Distribution', 'aws.cloudformation.stack': 'aws.cloudformation.Stack', + 'aws.cloudsearch.domain': 'aws.cloudsearch.Domain', 'aws.cloudwatch.alarm': 'aws.cloudwatch.Alarm', 'aws.dynamodb.table': 'aws.dynamodb.Table', 'aws.ec2.address': 'aws.ec2.Address', @@ -40,12 +42,17 @@ 'aws.ec2.vpc-peering-connection': 'aws.ec2.VpcPeeringConnection', 'aws.ec2.subnet': 'aws.ec2.Subnet', 'aws.ec2.launch-template': 'aws.ec2.LaunchTemplate', + 'aws.ecs.cluster': 'aws.ecs.Cluster', + 'aws.ecs.task-definition': 'aws.ecs.TaskDefinition', + 'aws.efs.filesystem': 'aws.efs.Filesystem', 'aws.elasticache.cluster': 'aws.elasticache.Cluster', 'aws.elasticache.subnet-group': 'aws.elasticache.SubnetGroup', 'aws.elasticache.snapshot': 'aws.elasticache.Snapshot', 'aws.elasticbeanstalk.application': 'aws.elasticbeanstalk.Application', 'aws.elasticbeanstalk.environment': 'aws.elasticbeanstalk.Environment', 'aws.elb.loadbalancer': 'aws.elb.LoadBalancer', + 'aws.elbv2.loadbalancer': 'aws.elbv2.LoadBalancer', + 'aws.elbv2.targetgroup': 'aws.elbv2.TargetGroup', 'aws.es.domain': 'aws.es.ElasticsearchDomain', 'aws.firehose.deliverystream': 'aws.firehose.DeliveryStream', 'aws.iam.group': 'aws.iam.Group', @@ -56,6 +63,7 @@ 'aws.iam.server-certificate': 'aws.iam.ServerCertificate', 'aws.kinesis.stream': 'aws.kinesis.Stream', 'aws.lambda.function': 'aws.lambda.Function', + 'aws.opsworks.stack': 'aws.opsworks.Stack', 'aws.rds.db': 'aws.rds.DBInstance', 'aws.rds.secgrp': 'aws.rds.DBSecurityGroup', 'aws.redshift.cluster': 'aws.redshift.Cluster', @@ -63,8 +71,10 @@ 'aws.route53.healthcheck': 'aws.route53.HealthCheck', 'aws.s3.bucket': 'aws.s3.Bucket', 'aws.sqs.queue': 'aws.sqs.Queue', + 'aws.ses.identity': 'aws.ses.Identity', 'aws.sns.subscription': 'aws.sns.Subscription', - 'aws.sns.topic': 'aws.sns.Topic' + 'aws.sns.topic': 'aws.sns.Topic', + 'aws.support.check': 'aws.support.Check' } diff --git a/skew/resources/aws/__init__.py b/skew/resources/aws/__init__.py index d3dd49c..a2eadc2 100644 --- a/skew/resources/aws/__init__.py +++ b/skew/resources/aws/__init__.py @@ -47,6 +47,7 @@ class AWSResource(Resource): Each entry in the dictionary we define: * service - The AWS service in which this resource is defined. + * resourcegroups_tagging - Precise if the resource supports tags. * enum_spec - The enumeration configuration. This is a tuple consisting of the name of the operation to call to enumerate the resources, a jmespath query that will be run against the result of the operation @@ -169,12 +170,16 @@ def tags(self): _tags = self.data['Tags'] if isinstance(_tags, list): for kvpair in _tags: - if kvpair['Key'] in self._tags: - if not isinstance(self._tags[kvpair['Key']], list): - self._tags[kvpair['Key']] = [self._tags[kvpair['Key']]] - self._tags[kvpair['Key']].append(kvpair['Value']) + # Compatibility fix for ECS, that use lowercase 'key' and 'value' as dict keys + # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs.html#ECS.Client.list_tags_for_resource + tags_key = kvpair.get('Key', kvpair.get('key')) + tags_value = kvpair.get('Value', kvpair.get('value')) + if tags_key in self._tags: + if not isinstance(self._tags[tags_key], list): + self._tags[tags_key] = [self._tags[tags_key]] + self._tags[tags_key].append(tags_value) else: - self._tags[kvpair['Key']] = kvpair['Value'] + self._tags[tags_key] = tags_value elif isinstance(_tags, dict): self._tags = _tags return self._tags diff --git a/skew/resources/aws/acm.py b/skew/resources/aws/acm.py new file mode 100644 index 0000000..147d295 --- /dev/null +++ b/skew/resources/aws/acm.py @@ -0,0 +1,60 @@ +# Copyright (c) 2014 Scopely, Inc. +# Copyright (c) 2015 Mitch Garnaat +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import logging + +import jmespath + +from skew.resources.aws import AWSResource + + +LOG = logging.getLogger(__name__) + + +class Certificate(AWSResource): + + class Meta(object): + service = 'acm' + type = 'certificate' + resourcegroups_tagging = True + enum_spec = ('list_certificates', 'CertificateSummaryList', None) + detail_spec = ('describe_certificate', 'CertificateArn', 'Certificate') + id = 'CertificateArn' + tags_spec = ('list_tags_for_certificate', 'Tags[]', + 'CertificateArn', 'id') + filter_name = None + name = 'DomainName' + date = 'CreatedAt' + dimension = None + + @classmethod + def filter(cls, arn, resource_id, data): + certificate_id = data.get(cls.Meta.id).split('/')[-1] + LOG.debug('%s == %s', resource_id, certificate_id) + return resource_id == certificate_id + + @property + def arn(self): + return self.data['CertificateArn'] + + def __init__(self, client, data, query=None): + super(Certificate, self).__init__(client, data, query) + + self._id = data['CertificateArn'] + + detail_op, param_name, detail_path = self.Meta.detail_spec + params = {param_name: data['CertificateArn']} + data = client.call(detail_op, **params) + + self.data = jmespath.search(detail_path, data) diff --git a/skew/resources/aws/apigateway.py b/skew/resources/aws/apigateway.py index a460a0a..2a61d9d 100644 --- a/skew/resources/aws/apigateway.py +++ b/skew/resources/aws/apigateway.py @@ -21,6 +21,7 @@ class RestAPI(AWSResource): class Meta(object): service = 'apigateway' type = 'restapis' + resourcegroups_tagging = False enum_spec = ('get_rest_apis', 'items', None) id = 'id' filter_name = None @@ -35,3 +36,10 @@ def filter(cls, arn, resource_id, data): api_id = data.get(cls.Meta.id) LOG.debug('%s == %s', resource_id, api_id) return resource_id == api_id + + @property + def arn(self): + # arn:aws:apigateway:us-east-1::/restapis/vwxyz12345 + return 'arn:aws:apigateway:%s::/restapis/%s' % ( + self._client.region_name, + self.id) diff --git a/skew/resources/aws/autoscaling.py b/skew/resources/aws/autoscaling.py index 0d84ee2..2eb880b 100644 --- a/skew/resources/aws/autoscaling.py +++ b/skew/resources/aws/autoscaling.py @@ -15,6 +15,7 @@ import jmespath from skew.resources.aws import AWSResource +from skew.awsclient import get_awsclient class AutoScalingGroup(AWSResource): @@ -22,6 +23,7 @@ class AutoScalingGroup(AWSResource): class Meta(object): service = 'autoscaling' type = 'autoScalingGroup' + resourcegroups_tagging = False name = 'AutoScalingGroupName' date = 'CreatedTime' dimension = 'AutoScalingGroupName' @@ -39,6 +41,33 @@ def __init__(self, client, data, query=None): def arn(self): return self._arn_query.search(self.data) + def sleek(self): + # Always render lists in the same order to avoid false changes detection + self.data['EnabledMetrics'].sort(key=lambda item: item['Metric']) + self.data['SuspendedProcesses'].sort(key=str) + + @classmethod + def set_tags(cls, arn, region, account, tags, resource_id=None, **kwargs): + client = get_awsclient( + cls.Meta.service, region, account, **kwargs) + asg_name = arn.split(':')[7].split('/')[1] + addon = dict(ResourceId=asg_name, + ResourceType='auto-scaling-group', + PropagateAtLaunch=False) + tags_list = [dict(Key=k, Value=str(v), **addon) for k, v in tags.items()] + return client.call('create_or_update_tags', Tags=tags_list) + + @classmethod + def unset_tags(cls, arn, region, account, tag_keys, resource_id=None, **kwargs): + client = get_awsclient( + cls.Meta.service, region, account, **kwargs) + asg_name = arn.split(':')[7].split('/')[1] + addon = dict(ResourceId=asg_name, + ResourceType='auto-scaling-group', + PropagateAtLaunch=False) + tags_list = [dict(Key=k, **addon) for k in tag_keys] + return client.call('delete_tags', Tags=tags_list) + class LaunchConfiguration(AWSResource): diff --git a/skew/resources/aws/cloudformation.py b/skew/resources/aws/cloudformation.py index 782af9f..96bc1e5 100644 --- a/skew/resources/aws/cloudformation.py +++ b/skew/resources/aws/cloudformation.py @@ -38,6 +38,7 @@ def enumerate(cls, arn, region, account, resource_id=None, **kwargs): class Meta(object): service = 'cloudformation' type = 'stack' + resourcegroups_tagging = False enum_spec = ('describe_stacks', 'Stacks[]', None) detail_spec = ('describe_stack_resources', 'StackName', 'StackResources[]') diff --git a/skew/resources/aws/cloudfront.py b/skew/resources/aws/cloudfront.py index 5b096fa..c54010a 100644 --- a/skew/resources/aws/cloudfront.py +++ b/skew/resources/aws/cloudfront.py @@ -1,6 +1,7 @@ import logging from skew.resources.aws import AWSResource +from skew.awsclient import get_awsclient LOG = logging.getLogger(__name__) @@ -20,12 +21,13 @@ class Distribution(CloudfrontResource): class Meta(object): service = 'cloudfront' type = 'distribution' + resourcegroups_tagging = False enum_spec = ('list_distributions', 'DistributionList.Items[]', None) detail_spec = None id = 'Id' tags_spec = ('list_tags_for_resource', 'Tags.Items[]', 'Resource', 'arn') - name = 'DomainName' + name = 'Id' filter_name = None date = 'LastModifiedTime' dimension = None @@ -34,3 +36,16 @@ class Meta(object): def filter(cls, arn, resource_id, data): LOG.debug('%s == %s', resource_id, data) return resource_id == data['Id'] + + @classmethod + def set_tags(cls, arn, region, account, tags, resource_id=None, **kwargs): + client = get_awsclient( + cls.Meta.service, region, account, **kwargs) + tags_list = [dict(Key=k, Value=str(v)) for k, v in tags.items()] + return client.call('tag_resource', Resource=arn, Tags=dict(Items=tags_list)) + + @classmethod + def unset_tags(cls, arn, region, account, tag_keys, resource_id=None, **kwargs): + client = get_awsclient( + cls.Meta.service, region, account, **kwargs) + return client.call('untag_resource', Resource=arn, TagKeys=dict(Items=tag_keys)) diff --git a/skew/resources/aws/cloudsearch.py b/skew/resources/aws/cloudsearch.py new file mode 100644 index 0000000..bb250cb --- /dev/null +++ b/skew/resources/aws/cloudsearch.py @@ -0,0 +1,42 @@ +# Copyright (c) 2014 Scopely, Inc. +# Copyright (c) 2015 Mitch Garnaat +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import logging + +import jmespath + +from skew.resources.aws import AWSResource + + +LOG = logging.getLogger(__name__) + + +class Domain(AWSResource): + + class Meta(object): + service = 'cloudsearch' + type = 'domain' + resourcegroups_tagging = False + enum_spec = ('describe_domains', 'DomainStatusList', None) + detail_spec = None + id = 'ARN' + tags_spec = None + filter_name = None + name = 'DomainName' + date = 'Created' + dimension = None + + @property + def arn(self): + return self.data['ARN'] diff --git a/skew/resources/aws/cloudwatch.py b/skew/resources/aws/cloudwatch.py index 89807bb..69916d0 100644 --- a/skew/resources/aws/cloudwatch.py +++ b/skew/resources/aws/cloudwatch.py @@ -20,6 +20,7 @@ class Alarm(AWSResource): class Meta(object): service = 'cloudwatch' type = 'alarm' + resourcegroups_tagging = False enum_spec = ('describe_alarms', 'MetricAlarms', None) id = 'AlarmArn' filter_name = 'AlarmNames' diff --git a/skew/resources/aws/dynamodb.py b/skew/resources/aws/dynamodb.py index 196c6b7..074ed69 100644 --- a/skew/resources/aws/dynamodb.py +++ b/skew/resources/aws/dynamodb.py @@ -27,6 +27,7 @@ class Table(AWSResource): class Meta(object): service = 'dynamodb' type = 'table' + resourcegroups_tagging = True enum_spec = ('list_tables', 'TableNames', None) detail_spec = ('describe_table', 'TableName', 'Table') id = 'Table' diff --git a/skew/resources/aws/ec2.py b/skew/resources/aws/ec2.py index 18253ad..b78b0e0 100644 --- a/skew/resources/aws/ec2.py +++ b/skew/resources/aws/ec2.py @@ -20,12 +20,13 @@ class Instance(AWSResource): class Meta(object): service = 'ec2' type = 'instance' + resourcegroups_tagging = True enum_spec = ('describe_instances', 'Reservations[].Instances[]', None) detail_spec = None id = 'InstanceId' filter_name = 'InstanceIds' filter_type = 'list' - name = 'PublicDnsName' + name = 'InstanceId' date = 'LaunchTime' dimension = 'InstanceId' @@ -33,12 +34,17 @@ class Meta(object): def parent(self): return self.data['ImageId'] + def __init__(self, client, data, query=None): + super(Instance, self).__init__(client, data, query) + # Asset name is get by tags if defined, or is InstanceId + self._name = self.tags.get('Name', self.data['InstanceId']) class SecurityGroup(AWSResource): class Meta(object): service = 'ec2' type = 'security-group' + resourcegroups_tagging = True enum_spec = ('describe_security_groups', 'SecurityGroups', None) detail_spec = None id = 'GroupId' @@ -54,6 +60,7 @@ class KeyPair(AWSResource): class Meta(object): service = 'ec2' type = 'key-pair' + resourcegroups_tagging = False enum_spec = ('describe_key_pairs', 'KeyPairs', None) detail_spec = None id = 'KeyName' @@ -68,6 +75,7 @@ class Address(AWSResource): class Meta(object): service = 'ec2' type = 'address' + resourcegroups_tagging = False enum_spec = ('describe_addresses', 'Addresses', None) detail_spec = None id = 'PublicIp' @@ -83,6 +91,7 @@ class Volume(AWSResource): class Meta(object): service = 'ec2' type = 'volume' + resourcegroups_tagging = True enum_spec = ('describe_volumes', 'Volumes', None) detail_spec = None id = 'VolumeId' @@ -105,6 +114,7 @@ class Snapshot(AWSResource): class Meta(object): service = 'ec2' type = 'snapshot' + resourcegroups_tagging = True enum_spec = ( 'describe_snapshots', 'Snapshots', {'OwnerIds': ['self']}) detail_spec = None @@ -128,6 +138,7 @@ class Image(AWSResource): class Meta(object): service = 'ec2' type = 'image' + resourcegroups_tagging = True enum_spec = ( 'describe_images', 'Images', {'Owners': ['self']}) detail_spec = None @@ -151,6 +162,7 @@ class Vpc(AWSResource): class Meta(object): service = 'ec2' type = 'vpc' + resourcegroups_tagging = True enum_spec = ('describe_vpcs', 'Vpcs', None) detail_spec = None id = 'VpcId' @@ -166,6 +178,7 @@ class Subnet(AWSResource): class Meta(object): service = 'ec2' type = 'subnet' + resourcegroups_tagging = True enum_spec = ('describe_subnets', 'Subnets', None) detail_spec = None id = 'SubnetId' @@ -181,6 +194,7 @@ class CustomerGateway(AWSResource): class Meta(object): service = 'ec2' type = 'customer-gateway' + resourcegroups_tagging = True enum_spec = ('describe_customer_gateways', 'CustomerGateway', None) detail_spec = None id = 'CustomerGatewayId' @@ -197,6 +211,7 @@ class Meta(object): service = 'ec2' type = 'internet-gateway' enum_spec = ('describe_internet_gateways', 'InternetGateways', None) + resourcegroups_tagging = True detail_spec = None id = 'InternetGatewayId' filter_name = 'InternetGatewayIds' @@ -211,6 +226,7 @@ class RouteTable(AWSResource): class Meta(object): service = 'ec2' type = 'route-table' + resourcegroups_tagging = True enum_spec = ('describe_route_tables', 'RouteTables', None) detail_spec = None id = 'RouteTableId' @@ -240,6 +256,7 @@ class NetworkAcl(AWSResource): class Meta(object): service = 'ec2' type = 'network-acl' + resourcegroups_tagging = True enum_spec = ('describe_network_acls', 'NetworkAcls', None) detail_spec = None id = 'NetworkAclId' @@ -255,6 +272,7 @@ class VpcPeeringConnection(AWSResource): class Meta(object): service = 'ec2' type = 'vpc-peering-connection' + resourcegroups_tagging = False enum_spec = ('describe_vpc_peering_connections', 'VpcPeeringConnection', None) detail_spec = None diff --git a/skew/resources/aws/ecs.py b/skew/resources/aws/ecs.py new file mode 100644 index 0000000..b539ee9 --- /dev/null +++ b/skew/resources/aws/ecs.py @@ -0,0 +1,80 @@ +# Copyright (c) 2014 Scopely, Inc. +# Copyright (c) 2015 Mitch Garnaat +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import logging + +import jmespath + +from skew.resources.aws import AWSResource + + +LOG = logging.getLogger(__name__) + + +class Cluster(AWSResource): + + class Meta(object): + service = 'ecs' + type = 'cluster' + resourcegroups_tagging = False + enum_spec = ('list_clusters', 'clusterArns', None) + detail_spec = ('describe_clusters', 'clusters', 'clusters[0]') + id = None + tags_spec = None # Not yet supported on Lambda boto3 included + # tags_spec = ('list_tags_for_resource', 'tags[]', + # 'resourceArn', 'arn') + + filter_name = None + name = 'clusterName' + date = None + dimension = None + + @property + def arn(self): + return self.data['clusterArn'] + + def __init__(self, client, data, query=None): + super(Cluster, self).__init__(client, data, query) + self._id = data + detail_op, param_name, detail_path = self.Meta.detail_spec + params = {param_name: [self.id]} + data = client.call(detail_op, **params) + self.data = jmespath.search(detail_path, data) + + +class TaskDefinition(AWSResource): + + class Meta(object): + service = 'ecs' + type = 'task-definition' + resourcegroups_tagging = False + enum_spec = ('list_task_definitions', 'taskDefinitionArns', None) + detail_spec = ('describe_task_definition', 'taskDefinition', 'taskDefinition') + id = None + name = None + filter_name = None + date = None + dimension = None + + def __init__(self, client, data, query=None): + super(TaskDefinition, self).__init__(client, data, query) + self._id = data + detail_op, param_name, detail_path = self.Meta.detail_spec + params = {param_name: self.id} + data = client.call(detail_op, **params) + self.data = jmespath.search(detail_path, data) + + @property + def arn(self): + return self.data['taskDefinitionArn'] \ No newline at end of file diff --git a/skew/resources/aws/efs.py b/skew/resources/aws/efs.py new file mode 100644 index 0000000..85429e3 --- /dev/null +++ b/skew/resources/aws/efs.py @@ -0,0 +1,72 @@ +# Copyright (c) 2014 Scopely, Inc. +# Copyright (c) 2015 Mitch Garnaat +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import logging + +import jmespath + +from skew.resources.aws import AWSResource +from skew.awsclient import get_awsclient + + +LOG = logging.getLogger(__name__) + + +class Filesystem(AWSResource): + + class Meta(object): + service = 'efs' + type = 'filesystem' + resourcegroups_tagging = False + enum_spec = ('describe_file_systems', 'FileSystems', None) + detail_spec = None + id = 'FileSystemId' + tags_spec = ('describe_tags', 'Tags[]', + 'FileSystemId', 'id') + filter_name = None + name = 'Name' + date = 'CreationTime' + dimension = None + + @property + def arn(self): + # arn:aws:elasticfilesystem:us-east-1:123456789012:file-system-id/fs12345678 + return 'arn:aws:%s:%s:%s:%s/%s' % ( + 'elasticfilesystem', + self._client.region_name, + self._client.account_id, + 'file-system-id', self.id) + + def __init__(self, client, data, query=None): + super(Filesystem, self).__init__(client, data, query) + # Asset name is get by tags if defined, or is FileSystemId + self._name = self.tags.get('Name', self.data['FileSystemId']) + + def sleek(self): + self.data['SizeInBytes'] = 0 + + @classmethod + def set_tags(cls, arn, region, account, tags, resource_id=None, **kwargs): + client = get_awsclient( + cls.Meta.service, region, account, **kwargs) + tags_list = [dict(Key=k, Value=str(v)) for k, v in tags.items()] + x = client.call('create_tags', FileSystemId=arn.split('/')[-1], Tags=tags_list) + return x + + @classmethod + def unset_tags(cls, arn, region, account, tag_keys, resource_id=None, **kwargs): + client = get_awsclient( + cls.Meta.service, region, account, **kwargs) + x = client.call('delete_tags', FileSystemId=arn.split('/')[-1], TagKeys=tag_keys) + return x diff --git a/skew/resources/aws/elasticache.py b/skew/resources/aws/elasticache.py index bbfbfc9..7f5141f 100644 --- a/skew/resources/aws/elasticache.py +++ b/skew/resources/aws/elasticache.py @@ -20,6 +20,7 @@ class Cluster(AWSResource): class Meta(object): service = 'elasticache' type = 'cluster' + resourcegroups_tagging = True enum_spec = ('describe_cache_clusters', 'CacheClusters[]', None) detail_spec = None @@ -45,6 +46,7 @@ class SubnetGroup(AWSResource): class Meta(object): service = 'elasticache' type = 'subnet-group' + resourcegroups_tagging = False enum_spec = ('describe_cache_subnet_groups', 'CacheSubnetGroups', None) detail_spec = None @@ -61,6 +63,7 @@ class Snapshot(AWSResource): class Meta(object): service = 'elasticache' type = 'snapshot' + resourcegroups_tagging = True enum_spec = ('describe_snapshots', 'Snapshots', None) detail_spec = None id = 'SnapshotName' diff --git a/skew/resources/aws/elasticbeanstalk.py b/skew/resources/aws/elasticbeanstalk.py index be2e598..7b8c31a 100644 --- a/skew/resources/aws/elasticbeanstalk.py +++ b/skew/resources/aws/elasticbeanstalk.py @@ -17,6 +17,7 @@ class Application(AWSResource): class Meta(object): service = 'elasticbeanstalk' type = 'application' + resourcegroups_tagging = False enum_spec = ('describe_applications', 'Applications', None) detail_spec = None id = 'ApplicationName' @@ -31,6 +32,7 @@ class Environment(AWSResource): class Meta(object): service = 'elasticbeanstalk' type = 'environment' + resourcegroups_tagging = True enum_spec = ('describe_environments', 'Environments', None) detail_spec = None id = 'EnvironmentName' @@ -38,4 +40,8 @@ class Meta(object): filter_type = None name = 'EnvironmentName' date = None - dimension = None \ No newline at end of file + dimension = None + + @property + def arn(self): + return self.data['EnvironmentArn'] diff --git a/skew/resources/aws/elb.py b/skew/resources/aws/elb.py index 1938d7c..c03db69 100644 --- a/skew/resources/aws/elb.py +++ b/skew/resources/aws/elb.py @@ -20,14 +20,22 @@ class LoadBalancer(AWSResource): class Meta(object): service = 'elb' type = 'loadbalancer' + resourcegroups_tagging = True enum_spec = ('describe_load_balancers', 'LoadBalancerDescriptions', None) detail_spec = None id = 'LoadBalancerName' filter_name = 'LoadBalancerNames' filter_type = 'list' - name = 'DNSName' + name = 'LoadBalancerName' date = 'CreatedTime' dimension = 'LoadBalancerName' tags_spec = ('describe_tags', 'TagDescriptions[].Tags[]', 'LoadBalancerNames', 'id') + + @property + def arn(self): + return 'arn:aws:elasticloadbalancing:%s:%s:%s/%s' % ( + self._client.region_name, + self._client.account_id, + self.resourcetype, self.id) diff --git a/skew/resources/aws/elbv2.py b/skew/resources/aws/elbv2.py new file mode 100644 index 0000000..bb2d4de --- /dev/null +++ b/skew/resources/aws/elbv2.py @@ -0,0 +1,71 @@ +# Copyright (c) 2014 Scopely, Inc. +# Copyright (c) 2015 Mitch Garnaat +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import jmespath + +from skew.resources.aws import AWSResource + + +class LoadBalancer(AWSResource): + + class Meta(object): + service = 'elbv2' + type = 'loadbalancer' + resourcegroups_tagging = True + enum_spec = ('describe_load_balancers', + 'LoadBalancers', None) + # detail_spec = None + detail_spec = ('describe_listeners', 'LoadBalancerArn', 'Listeners') + id = 'LoadBalancerArn' + filter_name = 'Names' + filter_type = 'list' + name = 'LoadBalancerName' + date = 'CreatedTime' + dimension = None + tags_spec = ('describe_tags', 'TagDescriptions[].Tags[]', + 'ResourceArns', 'id') + + @property + def arn(self): + return self.data['LoadBalancerArn'] + + def __init__(self, client, data, query=None): + super(LoadBalancer, self).__init__(client, data, query) + detail_op, param_name, detail_path = self.Meta.detail_spec + params = {param_name: self.data['LoadBalancerArn']} + data = client.call(detail_op, **params) + self.data['Listeners'] = jmespath.search(detail_path, data) + + +class TargetGroup(AWSResource): + + class Meta(object): + service = 'elbv2' + type = 'targetgroup' + resourcegroups_tagging = False + enum_spec = ('describe_target_groups', + 'TargetGroups', None) + detail_spec = None + id = 'TargetGroupArn' + filter_name = 'Names' + filter_type = 'list' + name = 'TargetGroupName' + date = 'CreatedTime' + dimension = 'LoadBalancerName' + tags_spec = ('describe_tags', 'TagDescriptions[].Tags[]', + 'LoadBalancerNames', 'id') + + @property + def arn(self): + return self.data['TargetGroupArn'] diff --git a/skew/resources/aws/es.py b/skew/resources/aws/es.py index 3243ebf..8a13247 100644 --- a/skew/resources/aws/es.py +++ b/skew/resources/aws/es.py @@ -22,6 +22,7 @@ class ElasticsearchDomain(AWSResource): class Meta(object): service = 'es' type = 'domain' + resourcegroups_tagging = True enum_spec = ('list_domain_names', 'DomainNames[].DomainName', None) tags_spec = ('list_tags', 'TagList', 'ARN', 'arn') diff --git a/skew/resources/aws/firehose.py b/skew/resources/aws/firehose.py index f498b9d..103f7dc 100644 --- a/skew/resources/aws/firehose.py +++ b/skew/resources/aws/firehose.py @@ -18,6 +18,7 @@ class DeliveryStream(AWSResource): class Meta(object): service = 'firehose' type = 'deliverystream' + resourcegroups_tagging = False enum_spec = ('list_delivery_streams', 'DeliveryStreamNames', None) detail_spec = ('describe_delivery_stream', 'DeliveryStreamName', 'DeliveryStreamDescription') id = 'DeliveryStreamName' diff --git a/skew/resources/aws/iam.py b/skew/resources/aws/iam.py index 3485640..cd70514 100644 --- a/skew/resources/aws/iam.py +++ b/skew/resources/aws/iam.py @@ -34,6 +34,7 @@ class Group(IAMResource): class Meta(object): service = 'iam' type = 'group' + resourcegroups_tagging = False enum_spec = ('list_groups', 'Groups', None) detail_spec = None id = 'GroupId' @@ -53,6 +54,7 @@ class User(IAMResource): class Meta(object): service = 'iam' type = 'user' + resourcegroups_tagging = False enum_spec = ('list_users', 'Users', None) detail_spec = None id = 'UserId' @@ -72,6 +74,7 @@ class Role(IAMResource): class Meta(object): service = 'iam' type = 'role' + resourcegroups_tagging = False enum_spec = ('list_roles', 'Roles', None) detail_spec = None id = 'RoleId' @@ -91,6 +94,7 @@ class InstanceProfile(IAMResource): class Meta(object): service = 'iam' type = 'instance-profile' + resourcegroups_tagging = False enum_spec = ('list_instance_profiles', 'InstanceProfiles', None) detail_spec = None id = 'InstanceProfileId' @@ -110,6 +114,7 @@ class Policy(IAMResource): class Meta(object): service = 'iam' type = 'policy' + resourcegroups_tagging = False enum_spec = ('list_policies', 'Policies', None) detail_spec = None id = 'PolicyId' @@ -129,6 +134,7 @@ class ServerCertificate(IAMResource): class Meta(object): service = 'iam' type = 'server-certificate' + resourcegroups_tagging = False enum_spec = ('list_server_certificates', 'ServerCertificateMetadataList', None) diff --git a/skew/resources/aws/kinesis.py b/skew/resources/aws/kinesis.py index 69f009c..4d6f4b3 100644 --- a/skew/resources/aws/kinesis.py +++ b/skew/resources/aws/kinesis.py @@ -20,6 +20,7 @@ class Stream(AWSResource): class Meta(object): service = 'kinesis' type = 'stream' + resourcegroups_tagging = True enum_spec = ('list_streams', 'StreamNames', None) detail_spec = None id = 'StreamName' diff --git a/skew/resources/aws/lambda.py b/skew/resources/aws/lambda.py index c42cc87..b3cfe4d 100644 --- a/skew/resources/aws/lambda.py +++ b/skew/resources/aws/lambda.py @@ -37,6 +37,7 @@ def enumerate(cls, arn, region, account, resource_id=None, **kwargs): class Meta(object): service = 'lambda' type = 'function' + resourcegroups_tagging = True enum_spec = ('list_functions', 'Functions', None) detail_spec = None id = 'FunctionName' diff --git a/skew/resources/aws/opsworks.py b/skew/resources/aws/opsworks.py new file mode 100644 index 0000000..ee6fa6a --- /dev/null +++ b/skew/resources/aws/opsworks.py @@ -0,0 +1,60 @@ +# Copyright (c) 2014 Scopely, Inc. +# Copyright (c) 2015 Mitch Garnaat +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import logging + +from skew.resources.aws import AWSResource +from skew.awsclient import get_awsclient + + +LOG = logging.getLogger(__name__) + + +class Stack(AWSResource): + + class Meta(object): + service = 'opsworks' + type = 'stack' + resourcegroups_tagging = False + enum_spec = ('describe_stacks', 'Stacks', None) + detail_spec = None + id = 'StackId' + filter_name = None + name = 'Name' + date = 'CreatedAt' + dimension = None + tags_spec = ('list_tags', 'Tags', + 'ResourceArn', 'arn') + + @property + def arn(self): + return self.data.get('Arn') + + @classmethod + def set_tags(cls, arn, region, account, tags, resource_id=None, **kwargs): + # ResourceGroupsTaggingAPI supports regional stacks, but not classic (us-east-1) + # opsworks.tag_resource() supports both + client = get_awsclient( + cls.Meta.service, region, account, **kwargs) + r = client.call('tag_resource', ResourceArn=arn, Tags=tags) + LOG.debug('Tag ARN %s, r=%s', arn, r) + + @classmethod + def unset_tags(cls, arn, region, account, tag_keys, resource_id=None, **kwargs): + # ResourceGroupsTaggingAPI supports regional stacks, but not classic (us-east-1) + # opsworks.untag_resource() supports both + client = get_awsclient( + cls.Meta.service, region, account, **kwargs) + r = client.call('untag_resource', ResourceArn=arn, TagKeys=tag_keys) + LOG.debug('UnTag ARN %s, r=%s', arn, r) diff --git a/skew/resources/aws/rds.py b/skew/resources/aws/rds.py index e943766..ebe1fc6 100644 --- a/skew/resources/aws/rds.py +++ b/skew/resources/aws/rds.py @@ -20,6 +20,7 @@ class DBInstance(AWSResource): class Meta(object): service = 'rds' type = 'db' + resourcegroups_tagging = True enum_spec = ('describe_db_instances', 'DBInstances', None) tags_spec = ('list_tags_for_resource', 'TagList', 'ResourceName', 'arn') @@ -27,7 +28,7 @@ class Meta(object): id = 'DBInstanceIdentifier' filter_name = 'DBInstanceIdentifier' filter_type = 'scalar' - name = 'Endpoint.Address' + name = 'DBInstanceIdentifier' date = 'InstanceCreateTime' dimension = 'DBInstanceIdentifier' @@ -38,12 +39,16 @@ def arn(self): self._client.region_name, self._client.account_id, self.resourcetype, self.id) + def sleek(self): + self.data['LatestRestorableTime'] = '' + class DBSecurityGroup(AWSResource): class Meta(object): service = 'rds' type = 'secgrp' + resourcegroups_tagging = True enum_spec = ('describe_db_security_groups', 'DBSecurityGroups', None) detail_spec = None id = 'DBSecurityGroupName' @@ -59,4 +64,3 @@ def arn(self): self._client.service_name, self._client.region_name, self._client.account_id, self.resourcetype, self.id) - diff --git a/skew/resources/aws/redshift.py b/skew/resources/aws/redshift.py index a39430e..07e974d 100644 --- a/skew/resources/aws/redshift.py +++ b/skew/resources/aws/redshift.py @@ -20,6 +20,7 @@ class Cluster(AWSResource): class Meta(object): service = 'redshift' type = 'cluster' + resourcegroups_tagging = True enum_spec = ('describe_clusters', 'Clusters', None) detail_spec = None id = 'ClusterIdentifier' diff --git a/skew/resources/aws/route53.py b/skew/resources/aws/route53.py index 5c07a94..21460fe 100644 --- a/skew/resources/aws/route53.py +++ b/skew/resources/aws/route53.py @@ -28,6 +28,7 @@ class HostedZone(Route53Resource): class Meta(object): service = 'route53' type = 'hostedzone' + resourcegroups_tagging = True enum_spec = ('list_hosted_zones', 'HostedZones', None) detail_spec = ('GetHostedZone', 'Id', None) id = 'Id' @@ -42,12 +43,17 @@ class Meta(object): def id(self): return self._id.split('/')[-1] + @property + def arn(self): + return 'arn:aws:route:::hostedzone/{}'.format(self.id) + class HealthCheck(Route53Resource): class Meta(object): service = 'route53' type = 'healthcheck' + resourcegroups_tagging = True enum_spec = ('list_health_checks', 'HealthChecks', None) detail_spec = ('GetHealthCheck', 'Id', None) id = 'Id' @@ -64,6 +70,7 @@ class ResourceRecordSet(Route53Resource): class Meta(object): service = 'route53' type = 'rrset' + resourcegroups_tagging = False enum_spec = ('list_resource_record_sets', 'ResourceRecordSets', None) detail_spec = None id = 'Name' diff --git a/skew/resources/aws/s3.py b/skew/resources/aws/s3.py index 98a2b25..315f152 100644 --- a/skew/resources/aws/s3.py +++ b/skew/resources/aws/s3.py @@ -39,7 +39,7 @@ def enumerate(cls, arn, region, account, resource_id=None, **kwargs): location = response.get('LocationConstraint', 'us-east-1') if location is None: location = 'us-east-1' - if location is 'EU': + if location == 'EU': location = 'eu-west-1' cls._location_cache[r.id] = location if location == region: @@ -49,11 +49,12 @@ def enumerate(cls, arn, region, account, resource_id=None, **kwargs): class Meta(object): service = 's3' type = 'bucket' + resourcegroups_tagging = True enum_spec = ('list_buckets', 'Buckets[]', None) detail_spec = ('list_objects', 'Bucket', 'Contents[]') id = 'Name' filter_name = None - name = 'BucketName' + name = 'Name' date = 'CreationDate' dimension = None tags_spec = ('get_bucket_tagging', 'TagSet[]', @@ -72,3 +73,7 @@ def __iter__(self): self._keys = jmespath.search(detail_path, data) for key in self._keys: yield key + + @property + def arn(self): + return 'arn:aws:s3:::{}'.format(self.data['Name']) diff --git a/skew/resources/aws/ses.py b/skew/resources/aws/ses.py new file mode 100644 index 0000000..ec7608c --- /dev/null +++ b/skew/resources/aws/ses.py @@ -0,0 +1,45 @@ +# Copyright (c) 2014 Scopely, Inc. +# Copyright (c) 2015 Mitch Garnaat +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import logging + +import jmespath + +from skew.resources.aws import AWSResource + + +LOG = logging.getLogger(__name__) + + +class Identity(AWSResource): + + class Meta(object): + service = 'ses' + type = 'identity' + resourcegroups_tagging = False + enum_spec = ('list_identities', 'Identities', None) + detail_spec = ('describe_table', 'TableName', 'Table') + id = 'Identity' + tags_spec = None + filter_name = None + name = 'IdentityName' + date = None + dimension = 'IdentityName' + + @property + def arn(self): + return 'arn:aws:%s:%s:%s:%s/%s' % ( + self._client.service_name, + self._client.region_name, + self._client.account_id, self.resourcetype, self.data) diff --git a/skew/resources/aws/sns.py b/skew/resources/aws/sns.py index cf0c8ef..9c772ab 100644 --- a/skew/resources/aws/sns.py +++ b/skew/resources/aws/sns.py @@ -23,6 +23,7 @@ class Topic(AWSResource): class Meta(object): service = 'sns' type = 'topic' + resourcegroups_tagging = False enum_spec = ('list_topics', 'Topics', None) detail_spec = ('get_topic_attributes', 'TopicArn', 'Attributes') id = 'TopicArn' @@ -53,6 +54,9 @@ def __init__(self, client, data, query=None): self.data = jmespath.search(detail_path, data) + # Attribute DisplayName is not relevant, set Id as name instead + self._name = self._id + class Subscription(AWSResource): @@ -61,6 +65,7 @@ class Subscription(AWSResource): class Meta(object): service = 'sns' type = 'subscription' + resourcegroups_tagging = False enum_spec = ('list_subscriptions', 'Subscriptions', None) detail_spec = ('get_subscription_attributes', 'SubscriptionArn', 'Attributes') diff --git a/skew/resources/aws/sqs.py b/skew/resources/aws/sqs.py index 9517bcb..4af732b 100644 --- a/skew/resources/aws/sqs.py +++ b/skew/resources/aws/sqs.py @@ -13,6 +13,7 @@ # language governing permissions and limitations under the License. from skew.resources.aws import AWSResource +from skew.awsclient import get_awsclient class Queue(AWSResource): @@ -20,17 +21,52 @@ class Queue(AWSResource): class Meta(object): service = 'sqs' type = 'queue' + resourcegroups_tagging = False enum_spec = ('list_queues', 'QueueUrls', None) detail_spec = ('get_queue_attributes', 'QueueUrl', 'QueueUrl') id = 'QueueUrl' filter_name = 'QueueNamePrefix' filter_type = 'scalar' - name = 'QueueUrl' + name = 'QueueName' date = None dimension = 'QueueName' + tags_spec = None def __init__(self, client, data, query=None): super(Queue, self).__init__(client, data, query) self.data = {self.Meta.id: data, 'QueueName': data.split('/')[-1]} self._id = self.data['QueueName'] + response = client.call('list_queue_tags', + QueueUrl=self.data['QueueUrl']) + self._tags = response.get('Tags', {}) + + @property + def arn(self): + return 'arn:aws:sqs:%s:%s:%s' % ( + self._client.region_name, + self._client.account_id, self.id) + + @classmethod + def set_tags(cls, arn, region, account, tags, resource_id=None, **kwargs): + queue_name = arn.split(':')[5] + queue_url = 'https://sqs.{}.amazonaws.com/{}/{}'.format(region, + account, + queue_name) + client = get_awsclient( + cls.Meta.service, region, account, **kwargs) + return client.call('tag_queue', + QueueUrl=queue_url, + Tags=tags) + + @classmethod + def unset_tags(cls, arn, region, account, tag_keys, resource_id=None, **kwargs): + queue_name = arn.split(':')[5] + queue_url = 'https://sqs.{}.amazonaws.com/{}/{}'.format(region, + account, + queue_name) + client = get_awsclient( + cls.Meta.service, region, account, **kwargs) + return client.call('untag_queue', + QueueUrl=queue_url, + TagKeys=tag_keys) diff --git a/skew/resources/aws/support.py b/skew/resources/aws/support.py new file mode 100644 index 0000000..d957140 --- /dev/null +++ b/skew/resources/aws/support.py @@ -0,0 +1,38 @@ +# Copyright (c) 2014 Scopely, Inc. +# Copyright (c) 2015 Mitch Garnaat +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import logging + +import jmespath + +from skew.resources.aws import AWSResource + + +LOG = logging.getLogger(__name__) + + +class Check(AWSResource): + + class Meta(object): + service = 'support' + type = 'check' + resourcegroups_tagging = False + enum_spec = ('describe_trusted_advisor_checks', 'checks', None) + detail_spec = None + id = 'id' + tags_spec = None + filter_name = None + name = 'name' + date = None + dimension = 'IdentityName' diff --git a/skew/resources/resource.py b/skew/resources/resource.py index 92514cd..a6c6f35 100644 --- a/skew/resources/resource.py +++ b/skew/resources/resource.py @@ -69,6 +69,28 @@ def enumerate(cls, arn, region, account, resource_id=None, **kwargs): resources.append(cls(client, d, arn.query)) return resources + @classmethod + def set_tags(cls, arn, region, account, tags, resource_id=None, **kwargs): + if hasattr(cls.Meta, 'resourcegroups_tagging') and (cls.Meta.resourcegroups_tagging): + client = skew.awsclient.get_awsclient( + 'resourcegroupstaggingapi', region, account, **kwargs) + r = client.call('tag_resources', ResourceARNList=[arn], Tags=tags) + LOG.debug('Tag ARN %s, r=%s', arn, r) + + @classmethod + def unset_tags(cls, arn, region, account, tag_keys, resource_id=None, **kwargs): + if hasattr(cls.Meta, 'resourcegroups_tagging') and (cls.Meta.resourcegroups_tagging): + client = skew.awsclient.get_awsclient( + 'resourcegroupstaggingapi', region, account, **kwargs) + r = client.call('untag_resources', ResourceARNList=[arn], TagKeys=tag_keys) + LOG.debug('UnTag ARN %s, r=%s', arn, r) + + def sleek(self): + """ + overrides datas frequently varied by a static value. + """ + pass + class Meta(object): type = 'resource' dimension = None