Source code for c7n.resources.route53

# Copyright 2016-2017 Capital One Services, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License 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.
from __future__ import absolute_import, division, print_function, unicode_literals

import functools
import fnmatch
import json
import itertools
import os

from botocore.paginate import Paginator

from c7n.query import QueryResourceManager, ChildResourceManager
from c7n.manager import resources
from c7n.utils import chunks, get_retry, generate_arn, local_session, type_schema
from c7n.actions import BaseAction
from c7n.filters import Filter

from c7n.resources.shield import IsShieldProtected, SetShieldProtection
from c7n.tags import RemoveTag, Tag


[docs]class Route53Base(object): permissions = ('route53:ListTagsForResources',) retry = staticmethod(get_retry(('Throttled',))) @property def generate_arn(self): if self._generate_arn is None: self._generate_arn = functools.partial( generate_arn, self.get_model().service, resource_type=self.get_model().type) return self._generate_arn
[docs] def get_arn(self, r): return self.generate_arn(r[self.get_model().id].split("/")[-1])
[docs] def augment(self, resources): _describe_route53_tags( self.get_model(), resources, self.session_factory, self.executor_factory, self.retry) return resources
def _describe_route53_tags( model, resources, session_factory, executor_factory, retry): def process_tags(resources): client = local_session(session_factory).client('route53') resource_map = {} for r in resources: k = r[model.id] if "hostedzone" in k: k = k.split("/")[-1] resource_map[k] = r for resource_batch in chunks(list(resource_map.keys()), 10): results = retry( client.list_tags_for_resources, ResourceType=model.type, ResourceIds=resource_batch) for resource_tag_set in results['ResourceTagSets']: if ('ResourceId' in resource_tag_set and resource_tag_set['ResourceId'] in resource_map): resource_map[resource_tag_set['ResourceId']]['Tags'] = resource_tag_set['Tags'] with executor_factory(max_workers=2) as w: return list(w.map(process_tags, chunks(resources, 20)))
[docs]@resources.register('hostedzone') class HostedZone(Route53Base, QueryResourceManager):
[docs] class resource_type(object): service = 'route53' type = 'hostedzone' enum_spec = ('list_hosted_zones', 'HostedZones', None) # detail_spec = ('get_hosted_zone', 'Id', 'Id', None) id = 'Id' filter_name = None name = 'Name' date = None dimension = None universal_taggable = True # Denotes this resource type exists across regions global_resource = True
[docs] def get_arns(self, resource_set): arns = [] for r in resource_set: _id = r[self.get_model().id].split("/")[-1] arns.append(self.generate_arn(_id)) return arns
HostedZone.filter_registry.register('shield-enabled', IsShieldProtected) HostedZone.action_registry.register('set-shield', SetShieldProtection)
[docs]@resources.register('healthcheck') class HealthCheck(Route53Base, QueryResourceManager):
[docs] class resource_type(object): service = 'route53' type = 'healthcheck' enum_spec = ('list_health_checks', 'HealthChecks', None) name = id = 'Id' filter_name = None date = None dimension = None universal_taggable = True
[docs]@resources.register('rrset') class ResourceRecordSet(ChildResourceManager):
[docs] class resource_type(object): service = 'route53' type = 'rrset' parent_spec = ('hostedzone', 'HostedZoneId', None) enum_spec = ('list_resource_record_sets', 'ResourceRecordSets', None) name = id = 'Name' filter_name = None date = None dimension = None
[docs]@resources.register('r53domain') class Route53Domain(QueryResourceManager):
[docs] class resource_type(object): service = 'route53domains' type = 'r53domain' enum_spec = ('list_domains', 'Domains', None) name = id = 'DomainName' filter_name = None date = None dimension = None
permissions = ('route53domains:ListTagsForDomain',)
[docs] def augment(self, domains): client = local_session(self.session_factory).client('route53domains') def _list_tags(d): tags = client.list_tags_for_domain( DomainName=d['DomainName'])['TagList'] d['Tags'] = tags return d with self.executor_factory(max_workers=1) as w: return list(filter(None, w.map(_list_tags, domains)))
[docs]@Route53Domain.action_registry.register('tag') class Route53DomainAddTag(Tag): """Adds tags to a route53 domain :example: .. code-block: yaml policies: - name: route53-tag resource: r53domain filters: - "tag:DesiredTag": absent actions: - type: tag key: DesiredTag value: DesiredValue """ permissions = ('route53domains:UpdateTagsForDomain',)
[docs] def process_resource_set(self, client, domains, tags): mid = self.manager.resource_type.id for d in domains: client.update_tags_for_domain( DomainName=d[mid], TagsToUpdate=tags)
[docs]@Route53Domain.action_registry.register('remove-tag') class Route53DomainRemoveTag(RemoveTag): """Remove tags from a route53 domain :example: .. code-block: yaml policies: - name: route53-expired-tag resource: r53domain filters: - "tag:ExpiredTag": present actions: - type: remove-tag tags: ['ExpiredTag'] """ permissions = ('route53domains:DeleteTagsForDomain',)
[docs] def process_resource_set(self, client, domains, keys): for d in domains: client.delete_tags_for_domain( DomainName=d[self.id_key], TagsToDelete=keys)
[docs]@HostedZone.action_registry.register('set-query-logging') class SetQueryLogging(BaseAction): """Enables query logging on a hosted zone. By default this enables a log group per route53 domain, alternatively a log group name can be specified for a unified log across domains. Note this only applicable to public route53 domains, and log groups must be created in us-east-1 region. This action can optionally setup the resource permissions needed for route53 to log to cloud watch logs via `set-permissions: true`, else the cloud watch logs resource policy would need to be set separately. Its recommended to use a separate custodian policy on the log groups to set the log retention period for the zone logs. See `custodian schema aws.log-group.actions.set-retention` :example: .. code-block: yaml policies: - name: enablednsquerylogging resource: hostedzone region: us-east-1 filters: - type: query-logging-enabled state: false actions: - type: set-query-logging state: true """ permissions = ( 'route53:GetQueryLoggingConfig', 'route53:CreateQueryLoggingConfig', 'route53:DeleteQueryLoggingConfig', 'logs:DescribeLogGroups', 'logs:CreateLogGroups', 'logs:GetResourcePolicy', 'logs:PutResourcePolicy') schema = type_schema( 'set-query-logging', **{ 'set-permissions': {'type': 'boolean'}, 'log-group-prefix': {'type': 'string', 'default': '/aws/route53'}, 'log-group': {'type': 'string', 'default': 'auto'}, 'state': {'type': 'boolean'}}) statement = { "Sid": "Route53LogsToCloudWatchLogs", "Effect": "Allow", "Principal": {"Service": ["route53.amazonaws.com"]}, "Action": ["logs:PutLogEvents", "logs:CreateLogStream"], "Resource": None}
[docs] def validate(self): if not self.data.get('state', True): # By forcing use of a filter we ensure both getting to right set of # resources as well avoiding an extra api call here, as we'll reuse # the annotation from the filter for logging config. if not [f for f in self.manager.iter_filters() if isinstance( f, IsQueryLoggingEnabled)]: raise ValueError( "set-query-logging when deleting requires " "use of query-logging-enabled filter in policy") return self
[docs] def get_permissions(self): perms = [] if self.data.get('set-permissions'): perms.extend(('logs:GetResourcePolicy', 'logs:PutResourcePolicy')) if self.data.get('state', True): perms.append('route53:CreateQueryLoggingConfig') perms.append('logs:CreateLogGroups') perms.append('logs:DescribeLogGroups') perms.append('tag:GetResources') else: perms.append('route53:DeleteQueryLoggingConfig') return perms
[docs] def process(self, resources): if self.manager.config.region != 'us-east-1': self.log.warning("set-query-logging should be only be performed region: us-east-1") client = local_session(self.manager.session_factory).client('route53') state = self.data.get('state', True) zone_log_names = {z['Id']: self.get_zone_log_name(z) for z in resources} if state: self.ensure_log_groups(set(zone_log_names.values())) for r in resources: if not state: try: client.delete_query_logging_config(Id=r['c7n:log-config']['Id']) except client.exceptions.NoSuchQueryLoggingConfig: pass continue log_arn = "arn:aws:logs:us-east-1:{}:log-group:{}".format( self.manager.account_id, zone_log_names[r['Id']]) client.create_query_logging_config( HostedZoneId=r['Id'], CloudWatchLogsLogGroupArn=log_arn)
[docs] def get_zone_log_name(self, zone): if self.data.get('log-group', 'auto') == 'auto': log_group_name = "%s/%s" % ( self.data.get('log-group-prefix', '/aws/route53').rstrip('/'), zone['Name'][:-1]) else: log_group_name = self.data['log-group'] return log_group_name
[docs] def ensure_log_groups(self, group_names): log_manager = self.manager.get_resource_manager('log-group') log_manager.config = self.manager.config.copy(region='us-east-1') if len(group_names) == 1: groups = [] if log_manager.get_resources(list(group_names), augment=False): groups = [{'logGroupName': g} for g in group_names] else: common_prefix = os.path.commonprefix(group_names) if common_prefix not in ('', '/'): groups = log_manager.get_resources( [common_prefix], augment=False) else: groups = list(itertools.chain(*[ log_manager.get_resources([g]) for g in group_names])) missing = group_names.difference({g['logGroupName'] for g in groups}) # Logs groups must be created in us-east-1 for route53. client = local_session( self.manager.session_factory).client('logs', region_name='us-east-1') for g in missing: client.create_log_group(logGroupName=g) if self.data.get('set-permissions', False): self.ensure_route53_permissions(client, group_names)
[docs] def ensure_route53_permissions(self, client, group_names): if self.check_route53_permissions(client, group_names): return if self.data.get('log-group', 'auto') != 'auto': p_resource = "arn:aws:logs:us-east-1:{}:log-group:{}:*".format( self.manager.account_id, self.data['log-group']) else: p_resource = "arn:aws:logs:us-east-1:{}:log-group:{}/*".format( self.manager.account_id, self.data.get('log-group-prefix', '/aws/route53').rstrip('/')) statement = dict(self.statement) statement['Resource'] = p_resource client.put_resource_policy( policyName='Route53LogWrites', policyDocument=json.dumps( {"Version": "2012-10-17", "Statement": [statement]}))
[docs] def check_route53_permissions(self, client, group_names): group_names = set(group_names) for p in client.describe_resource_policies().get('resourcePolicies', []): for s in json.loads(p['policyDocument']).get('Statement', []): if (s['Effect'] == 'Allow' and s['Principal'].get('Service', ['']) == "route53.amazonaws.com"): group_names.difference_update( fnmatch.filter(group_names, s['Resource'].rsplit(':', 1)[-1])) if not group_names: return True return not bool(group_names)
[docs]def get_logging_config_paginator(client): return Paginator( client.list_query_logging_configs, {'input_token': 'NextToken', 'output_token': 'NextToken', 'result_key': 'QueryLoggingConfigs'}, client.meta.service_model.operation_model('ListQueryLoggingConfigs'))
[docs]@HostedZone.filter_registry.register('query-logging-enabled') class IsQueryLoggingEnabled(Filter): permissions = ('route53:GetQueryLoggingConfig', 'route53:GetHostedZone') schema = type_schema('query-logging-enabled', state={'type': 'boolean'})
[docs] def process(self, resources, event=None): client = local_session(self.manager.session_factory).client('route53') state = self.data.get('state', False) results = [] enabled_zones = { c['HostedZoneId']: c for c in get_logging_config_paginator( client).paginate().build_full_result().get( 'QueryLoggingConfigs', ())} for r in resources: zid = r['Id'].split('/', 2)[-1] # query logging is only supported for Public Hosted Zones. if r['Config']['PrivateZone'] is True: continue logging = zid in enabled_zones if logging and state: r['c7n:log-config'] = enabled_zones[zid] results.append(r) elif not logging and not state: results.append(r) return results