Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/c7n/resources/route53.py: 50%
470 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3import functools
4import fnmatch
5import json
6import itertools
7import os
9from botocore.paginate import Paginator
11from c7n.query import QueryResourceManager, ChildResourceManager, TypeInfo, RetryPageIterator
12from c7n.manager import resources
13from c7n.utils import chunks, get_retry, generate_arn, local_session, type_schema
14from c7n.actions import BaseAction
15from c7n.filters import Filter, ListItemFilter
16from c7n.resources.shield import IsShieldProtected, SetShieldProtection
17from c7n.tags import RemoveTag, Tag
18from c7n.filters.related import RelatedResourceFilter
19from c7n import tags, query
20from c7n.filters.iamaccess import CrossAccountAccessFilter
21from c7n.resolver import ValuesFrom
24class Route53Base:
26 permissions = ('route53:ListTagsForResources',)
27 retry = staticmethod(get_retry(('Throttled',)))
29 @property
30 def generate_arn(self):
31 if self._generate_arn is None:
32 self._generate_arn = functools.partial(
33 generate_arn,
34 self.get_model().service,
35 resource_type=self.get_model().arn_type)
36 return self._generate_arn
38 def get_arn(self, r):
39 return self.generate_arn(r[self.get_model().id].split("/")[-1])
41 def augment(self, resources):
42 _describe_route53_tags(
43 self.get_model(), resources, self.session_factory,
44 self.executor_factory, self.retry)
45 return resources
48def _describe_route53_tags(
49 model, resources, session_factory, executor_factory, retry):
51 def process_tags(resources):
52 client = local_session(session_factory).client('route53')
53 resource_map = {}
54 for r in resources:
55 k = r[model.id]
56 if "hostedzone" in k:
57 k = k.split("/")[-1]
58 resource_map[k] = r
60 for resource_batch in chunks(list(resource_map.keys()), 10):
61 results = retry(
62 client.list_tags_for_resources,
63 ResourceType=model.arn_type,
64 ResourceIds=resource_batch)
65 for resource_tag_set in results['ResourceTagSets']:
66 if ('ResourceId' in resource_tag_set and
67 resource_tag_set['ResourceId'] in resource_map):
68 resource_map[resource_tag_set['ResourceId']]['Tags'] = resource_tag_set['Tags']
70 with executor_factory(max_workers=2) as w:
71 return list(w.map(process_tags, chunks(resources, 20)))
74@resources.register('hostedzone')
75class HostedZone(Route53Base, QueryResourceManager):
77 class resource_type(TypeInfo):
78 service = 'route53'
79 arn_type = 'hostedzone'
80 enum_spec = ('list_hosted_zones', 'HostedZones', None)
81 # detail_spec = ('get_hosted_zone', 'Id', 'Id', None)
82 id = 'Id'
83 name = 'Name'
84 config_id = 'c7n:ConfigHostedZoneId'
85 universal_taggable = True
86 # Denotes this resource type exists across regions
87 global_resource = True
88 cfn_type = 'AWS::Route53::HostedZone'
90 def get_arns(self, resource_set):
91 arns = []
92 for r in resource_set:
93 _id = r[self.get_model().id].split("/")[-1]
94 arns.append(self.generate_arn(_id))
95 return arns
97 def augment(self, resources):
98 annotation_key = 'c7n:ConfigHostedZoneId'
99 resources = super(HostedZone, self).augment(resources)
100 for r in resources:
101 config_hzone_id = r['Id'].split("/")[-1]
102 r[annotation_key] = config_hzone_id
103 return resources
106HostedZone.filter_registry.register('shield-enabled', IsShieldProtected)
107HostedZone.action_registry.register('set-shield', SetShieldProtection)
110@resources.register('healthcheck')
111class HealthCheck(Route53Base, QueryResourceManager):
113 class resource_type(TypeInfo):
114 service = 'route53'
115 arn_type = 'healthcheck'
116 enum_spec = ('list_health_checks', 'HealthChecks', None)
117 name = id = 'Id'
118 universal_taggable = True
119 cfn_type = 'AWS::Route53::HealthCheck'
120 global_resource = True
123@resources.register('rrset')
124class ResourceRecordSet(ChildResourceManager):
126 class resource_type(TypeInfo):
127 service = 'route53'
128 arn_type = 'rrset'
129 parent_spec = ('hostedzone', 'HostedZoneId', True)
130 enum_spec = ('list_resource_record_sets', 'ResourceRecordSets', None)
131 name = id = 'Name'
132 cfn_type = 'AWS::Route53::RecordSet'
133 global_resource = True
136@resources.register('r53domain')
137class Route53Domain(QueryResourceManager):
139 class resource_type(TypeInfo):
140 service = 'route53domains'
141 arn_type = 'r53domain'
142 enum_spec = ('list_domains', 'Domains', None)
143 name = id = 'DomainName'
144 global_resource = False
146 permissions = ('route53domains:ListTagsForDomain',)
148 def augment(self, domains):
149 client = local_session(self.session_factory).client('route53domains')
150 for d in domains:
151 d['Tags'] = self.retry(
152 client.list_tags_for_domain,
153 DomainName=d['DomainName'])['TagList']
154 return domains
157@Route53Domain.action_registry.register('tag')
158class Route53DomainAddTag(Tag):
159 """Adds tags to a route53 domain
161 :example:
163 .. code-block:: yaml
165 policies:
166 - name: route53-tag
167 resource: r53domain
168 filters:
169 - "tag:DesiredTag": absent
170 actions:
171 - type: tag
172 key: DesiredTag
173 value: DesiredValue
174 """
175 permissions = ('route53domains:UpdateTagsForDomain',)
177 def process_resource_set(self, client, domains, tags):
178 mid = self.manager.resource_type.id
179 for d in domains:
180 client.update_tags_for_domain(
181 DomainName=d[mid],
182 TagsToUpdate=tags)
185@Route53Domain.action_registry.register('remove-tag')
186class Route53DomainRemoveTag(RemoveTag):
187 """Remove tags from a route53 domain
189 :example:
191 .. code-block:: yaml
193 policies:
194 - name: route53-expired-tag
195 resource: r53domain
196 filters:
197 - "tag:ExpiredTag": present
198 actions:
199 - type: remove-tag
200 tags: ['ExpiredTag']
201 """
202 permissions = ('route53domains:DeleteTagsForDomain',)
204 def process_resource_set(self, client, domains, keys):
205 for d in domains:
206 client.delete_tags_for_domain(
207 DomainName=d[self.id_key],
208 TagsToDelete=keys)
211@HostedZone.action_registry.register('delete')
212class Delete(BaseAction):
213 """Action to delete Route 53 hosted zones.
215 It is recommended to use a filter to avoid unwanted deletion of R53 hosted zones.
217 If set to force this action will wipe out all records in the hosted zone
218 before deleting the zone.
220 :example:
222 .. code-block:: yaml
224 policies:
225 - name: route53-delete-testing-hosted-zones
226 resource: aws.hostedzone
227 filters:
228 - 'tag:TestTag': present
229 actions:
230 - type: delete
231 force: true
233 """
235 schema = type_schema('delete', force={'type': 'boolean'})
236 permissions = ('route53:DeleteHostedZone',)
238 def process(self, hosted_zones):
239 client = local_session(self.manager.session_factory).client('route53')
240 error = None
241 for hz in hosted_zones:
242 if self.data.get('force'):
243 self.delete_records(client, hz)
244 try:
245 self.manager.retry(
246 client.delete_hosted_zone,
247 Id=hz['Id'],
248 ignore_err_codes=('NoSuchHostedZone'))
249 except client.exceptions.HostedZoneNotEmpty as e:
250 self.log.warning(
251 "HostedZone: %s cannot be deleted, "
252 "set force to remove all records in zone",
253 hz['Name'])
254 error = e
255 if error:
256 raise error
258 def delete_records(self, client, hz):
259 paginator = client.get_paginator('list_resource_record_sets')
260 paginator.PAGE_ITERATOR_CLS = RetryPageIterator
261 rrsets = paginator.paginate(HostedZoneId=hz['Id']).build_full_result()
263 for rrset in rrsets['ResourceRecordSets']:
264 # Trigger the deletion of all the resource record sets before deleting
265 # the hosted zone
267 # Exempt the two zone associated mandatory records
268 if rrset['Name'] == hz['Name'] and rrset['Type'] in ('NS', 'SOA'):
269 continue
270 self.manager.retry(
271 client.change_resource_record_sets,
272 HostedZoneId=hz['Id'],
273 ChangeBatch={
274 'Changes': [
275 {
276 'Action': 'DELETE',
277 'ResourceRecordSet': {
278 'Name': rrset['Name'],
279 'Type': rrset['Type'],
280 'TTL': rrset['TTL'],
281 'ResourceRecords': rrset['ResourceRecords']
282 },
283 }
284 ]
285 },
286 ignore_err_codes=('InvalidChangeBatch'))
289@HostedZone.action_registry.register('set-query-logging')
290class SetQueryLogging(BaseAction):
291 """Enables query logging on a hosted zone.
293 By default this enables a log group per route53 domain, alternatively
294 a log group name can be specified for a unified log across domains.
296 Note this only applicable to public route53 domains, and log groups
297 must be created in us-east-1 region.
299 This action can optionally setup the resource permissions needed for
300 route53 to log to cloud watch logs via `set-permissions: true`, else
301 the cloud watch logs resource policy would need to be set separately.
303 Its recommended to use a separate custodian policy on the log
304 groups to set the log retention period for the zone logs. See
305 `custodian schema aws.log-group.actions.set-retention`
307 :example:
309 .. code-block:: yaml
311 policies:
312 - name: enablednsquerylogging
313 resource: hostedzone
314 region: us-east-1
315 filters:
316 - type: query-logging-enabled
317 state: false
318 actions:
319 - type: set-query-logging
320 state: true
322 """
324 permissions = (
325 'route53:GetQueryLoggingConfig',
326 'route53:CreateQueryLoggingConfig',
327 'route53:DeleteQueryLoggingConfig',
328 'logs:DescribeLogGroups',
329 'logs:CreateLogGroup',
330 'logs:GetResourcePolicy',
331 'logs:PutResourcePolicy')
333 schema = type_schema(
334 'set-query-logging', **{
335 'set-permissions': {'type': 'boolean'},
336 'log-group-prefix': {'type': 'string', 'default': '/aws/route53'},
337 'log-group': {'type': 'string', 'default': 'auto'},
338 'state': {'type': 'boolean'}})
340 statement = {
341 "Sid": "Route53LogsToCloudWatchLogs",
342 "Effect": "Allow",
343 "Principal": {"Service": ["route53.amazonaws.com"]},
344 "Action": ["logs:PutLogEvents", "logs:CreateLogStream"],
345 "Resource": None}
347 def validate(self):
348 if not self.data.get('state', True):
349 # By forcing use of a filter we ensure both getting to right set of
350 # resources as well avoiding an extra api call here, as we'll reuse
351 # the annotation from the filter for logging config.
352 if not [f for f in self.manager.iter_filters() if isinstance(
353 f, IsQueryLoggingEnabled)]:
354 raise ValueError(
355 "set-query-logging when deleting requires "
356 "use of query-logging-enabled filter in policy")
357 return self
359 def get_permissions(self):
360 perms = []
361 if self.data.get('set-permissions'):
362 perms.extend(('logs:GetResourcePolicy', 'logs:PutResourcePolicy'))
363 if self.data.get('state', True):
364 perms.append('route53:CreateQueryLoggingConfig')
365 perms.append('logs:CreateLogGroup')
366 perms.append('logs:DescribeLogGroups')
367 perms.append('tag:GetResources')
368 else:
369 perms.append('route53:DeleteQueryLoggingConfig')
370 return perms
372 def process(self, resources):
373 if self.manager.config.region != 'us-east-1':
374 self.log.warning("set-query-logging should be only be performed region: us-east-1")
376 client = local_session(self.manager.session_factory).client('route53')
377 state = self.data.get('state', True)
379 zone_log_names = {z['Id']: self.get_zone_log_name(z) for z in resources}
380 if state:
381 self.ensure_log_groups(set(zone_log_names.values()))
383 for r in resources:
384 if not state:
385 try:
386 client.delete_query_logging_config(Id=r['c7n:log-config']['Id'])
387 except client.exceptions.NoSuchQueryLoggingConfig:
388 pass
389 continue
390 log_arn = "arn:aws:logs:us-east-1:{}:log-group:{}".format(
391 self.manager.account_id, zone_log_names[r['Id']])
392 client.create_query_logging_config(
393 HostedZoneId=r['Id'],
394 CloudWatchLogsLogGroupArn=log_arn)
396 def get_zone_log_name(self, zone):
397 if self.data.get('log-group', 'auto') == 'auto':
398 log_group_name = "%s/%s" % (
399 self.data.get('log-group-prefix', '/aws/route53').rstrip('/'),
400 zone['Name'][:-1])
401 else:
402 log_group_name = self.data['log-group']
403 return log_group_name
405 def ensure_log_groups(self, group_names):
406 log_manager = self.manager.get_resource_manager('log-group')
407 log_manager.config = self.manager.config.copy(region='us-east-1')
409 if len(group_names) == 1:
410 groups = []
411 if log_manager.get_resources(list(group_names), augment=False):
412 groups = [{'logGroupName': g} for g in group_names]
413 else:
414 common_prefix = os.path.commonprefix(list(group_names))
415 if common_prefix not in ('', '/'):
416 groups = log_manager.get_resources(
417 [common_prefix], augment=False)
418 else:
419 groups = list(itertools.chain(*[
420 log_manager.get_resources([g]) for g in group_names]))
422 missing = group_names.difference({g['logGroupName'] for g in groups})
424 # Logs groups must be created in us-east-1 for route53.
425 client = local_session(
426 self.manager.session_factory).client('logs', region_name='us-east-1')
428 for g in missing:
429 client.create_log_group(logGroupName=g)
431 if self.data.get('set-permissions', False):
432 self.ensure_route53_permissions(client, group_names)
434 def ensure_route53_permissions(self, client, group_names):
435 if self.check_route53_permissions(client, group_names):
436 return
437 if self.data.get('log-group', 'auto') != 'auto':
438 p_resource = "arn:aws:logs:us-east-1:{}:log-group:{}:*".format(
439 self.manager.account_id, self.data['log-group'])
440 else:
441 p_resource = "arn:aws:logs:us-east-1:{}:log-group:{}/*".format(
442 self.manager.account_id,
443 self.data.get('log-group-prefix', '/aws/route53').rstrip('/'))
445 statement = dict(self.statement)
446 statement['Resource'] = p_resource
448 client.put_resource_policy(
449 policyName='Route53LogWrites',
450 policyDocument=json.dumps(
451 {"Version": "2012-10-17", "Statement": [statement]}))
453 def check_route53_permissions(self, client, group_names):
454 group_names = set(group_names)
455 for p in client.describe_resource_policies().get('resourcePolicies', []):
456 for s in json.loads(p['policyDocument']).get('Statement', []):
457 if (s['Effect'] == 'Allow' and
458 s['Principal'].get('Service', ['']) == "route53.amazonaws.com"):
459 group_names.difference_update(
460 fnmatch.filter(group_names, s['Resource'].rsplit(':', 1)[-1]))
461 if not group_names:
462 return True
463 return not bool(group_names)
466def get_logging_config_paginator(client):
467 return Paginator(
468 client.list_query_logging_configs,
469 {'input_token': 'NextToken', 'output_token': 'NextToken',
470 'result_key': 'QueryLoggingConfigs'},
471 client.meta.service_model.operation_model('ListQueryLoggingConfigs'))
474@HostedZone.filter_registry.register('query-logging-enabled')
475class IsQueryLoggingEnabled(Filter):
477 permissions = ('route53:GetQueryLoggingConfig', 'route53:GetHostedZone',
478 'logs:DescribeSubscriptionFilters')
479 schema = type_schema('query-logging-enabled', state={'type': 'boolean'})
481 def process(self, resources, event=None):
482 client = local_session(self.manager.session_factory).client('route53')
483 cw_client = local_session(self.manager.session_factory).client('logs')
484 state = self.data.get('state', False)
485 results = []
487 enabled_zones = {
488 c['HostedZoneId']: c for c in
489 get_logging_config_paginator(
490 client).paginate().build_full_result().get(
491 'QueryLoggingConfigs', ())}
492 for r in resources:
493 zid = r['Id'].split('/', 2)[-1]
494 # query logging is only supported for Public Hosted Zones.
495 if r['Config']['PrivateZone'] is True:
496 continue
497 logging = zid in enabled_zones
498 if logging and state:
499 r['c7n:log-config'] = enabled_zones[zid]
500 log_group_name = r['c7n:log-config']['CloudWatchLogsLogGroupArn'].split(":")[6]
501 response = cw_client.describe_subscription_filters(logGroupName=log_group_name)
502 r['c7n:log-config']['loggroup_subscription'] = response['subscriptionFilters']
503 results.append(r)
504 elif not logging and not state:
505 results.append(r)
507 return results
510@resources.register('resolver-logs')
511class ResolverQueryLogConfig(QueryResourceManager):
513 class resource_type(TypeInfo):
514 service = 'route53resolver'
515 arn_type = 'resolver-query-log-config'
516 enum_spec = ('list_resolver_query_log_configs', 'ResolverQueryLogConfigs', None)
517 name = 'Name'
518 id = 'Id'
519 cfn_type = 'AWS::Route53Resolver::ResolverQueryLoggingConfig'
521 annotation_key = 'c7n:Associations'
522 permissions = (
523 'route53resolver:ListResolverQueryLogConfigs',
524 'route53resolver:ListResolverQueryLogConfigAssociations')
526 def augment(self, rqlcs):
527 client = local_session(self.session_factory).client('route53resolver')
528 for rqlc in rqlcs:
529 rqlc['Tags'] = self.retry(
530 client.list_tags_for_resource,
531 ResourceArn=rqlc['Arn'])['Tags']
532 rqlc[self.annotation_key] = client.list_resolver_query_log_config_associations().get(
533 'ResolverQueryLogConfigAssociations')
534 return rqlcs
537@ResolverQueryLogConfig.action_registry.register('associate-vpc')
538class ResolverQueryLogConfigAssociate(BaseAction):
539 """Associates ResolverQueryLogConfig to a VPC
541 :example:
543 .. code-block:: yaml
545 policies:
546 - name: r53-resolver-query-log-config-associate
547 resource: resolver-logs
548 filters:
549 - type: value
550 key: 'Name'
551 op: eq
552 value: "Test-rqlc"
553 actions:
554 - type: associate-vpc
555 vpcid: all
557 """
558 permissions = (
559 'route53resolver:AssociateResolverQueryLogConfig',
560 'route53resolver:ListResolverQueryLogConfigAssociations')
561 schema = type_schema('associate-vpc', vpcid={'type': 'string',
562 'pattern': '^(?:vpc-[0-9a-f]{8,17}|all)$'}, required=['vpcid'])
563 RelatedResource = 'c7n.resources.vpc.Vpc'
564 RelatedIdsExpression = 'ResourceArn'
566 def get_vpc_id(self):
567 vpcs = RelatedResourceFilter.get_resource_manager(self).resources()
568 if self.data.get('vpcid') == 'all':
569 vpc_ids = [v['VpcId'] for v in vpcs]
570 else:
571 vpc_ids = [self.data.get('vpcid')]
572 return vpc_ids
574 def is_associated(self, resource, vpc_id):
575 associated = False
576 for association in resource['c7n:Associations']:
577 if association['ResourceId'] == vpc_id:
578 associated = True
579 break
580 return associated
582 def process(self, resources):
583 client = local_session(self.manager.session_factory).client('route53resolver')
584 vpc_ids = self.get_vpc_id()
586 for resource in resources:
587 for vpc_id in vpc_ids:
588 if not self.is_associated(resource, vpc_id):
589 client.associate_resolver_query_log_config(
590 ResolverQueryLogConfigId=resource['Id'], ResourceId=vpc_id)
593@ResolverQueryLogConfig.filter_registry.register('is-associated')
594class LogConfigAssociationsFilter(Filter):
595 """ Checks LogConfig Associations for VPCs.
597 :example:
599 .. code-block:: yaml
601 policies:
602 - name: r53-resolver-query-log-config-associations
603 resource: resolver-logs
604 filters:
605 - type: is-associated
606 vpcid: "vpc-12345678"
608 """
609 permissions = ('route53resolver:ListResolverQueryLogConfigAssociations',)
610 schema = type_schema('is-associated',
611 vpcid={'type': 'string', 'pattern': '^(?:vpc-[0-9a-f]{8,17}|all)$'},)
612 RelatedResource = 'c7n.resources.vpc.Vpc'
613 RelatedIdsExpression = 'ResourceArn'
615 def process(self, resources, event=None):
616 results = []
617 vpc_ids = ResolverQueryLogConfigAssociate.get_vpc_id(self)
618 for resource in resources:
619 status = True
620 for vpc_id in vpc_ids:
621 if not ResolverQueryLogConfigAssociate.is_associated(self, resource, vpc_id):
622 status = False
623 break
625 if status:
626 results.append(resource)
628 return results
631# Readiness check in Amazon Route53 ARC is global feature. However,
632# US West (N. California) Region must be specified in Route53 ARC readiness check api call.
633# Please reference this AWS document:
634# https://docs.aws.amazon.com/r53recovery/latest/dg/introduction-regions.html
635ARC_REGION = 'us-west-2'
638class DescribeCheck(query.DescribeSource):
640 def augment(self, readiness_checks):
641 for r in readiness_checks:
642 Tags = self.manager.retry(
643 self.manager.get_client().list_tags_for_resources,
644 ResourceArn=r['ReadinessCheckArn'])['Tags']
645 r['Tags'] = [{'Key': k, "Value": v} for k, v in Tags.items()]
646 return readiness_checks
649@resources.register('readiness-check')
650class ReadinessCheck(QueryResourceManager):
652 class resource_type(TypeInfo):
653 service = 'route53-recovery-readiness'
654 arn_type = 'readiness-check'
655 enum_spec = ('list_readiness_checks', 'ReadinessChecks', None)
656 name = id = 'ReadinessCheckName'
657 global_resource = True
658 config_type = cfn_type = 'AWS::Route53RecoveryReadiness::ReadinessCheck'
660 source_mapping = {'describe': DescribeCheck, 'config': query.ConfigSource}
662 def get_client(self):
663 return local_session(self.session_factory) \
664 .client('route53-recovery-readiness', region_name=ARC_REGION)
667@ReadinessCheck.action_registry.register('tag')
668class ReadinessAddTag(Tag):
669 """Adds tags to a readiness check
671 :example:
673 .. code-block:: yaml
675 policies:
676 - name: readiness-tag
677 resource: readiness-check
678 filters:
679 - "tag:DesiredTag": absent
680 actions:
681 - type: tag
682 key: DesiredTag
683 value: DesiredValue
684 """
685 permissions = ('route53-recovery-readiness:TagResource',)
687 def get_client(self):
688 return self.manager.get_client()
690 def process_resource_set(self, client, readiness_checks, tags):
691 Tags = {r['Key']: r['Value'] for r in tags}
692 for r in readiness_checks:
693 client.tag_resource(
694 ResourceArn=r['ReadinessCheckArn'],
695 Tags=Tags)
698@ReadinessCheck.action_registry.register('remove-tag')
699class ReadinessCheckRemoveTag(RemoveTag):
700 """Remove tags from a readiness check
702 :example:
704 .. code-block:: yaml
706 policies:
707 - name: readiness-check-remove-tag
708 resource: readiness-check
709 filters:
710 - "tag:ExpiredTag": present
711 actions:
712 - type: remove-tag
713 tags: ['ExpiredTag']
714 """
715 permissions = ('route53-recovery-readiness:UntagResource',)
717 def get_client(self):
718 return self.manager.get_client()
720 def process_resource_set(self, client, readiness_checks, keys):
721 for r in readiness_checks:
722 client.untag_resource(
723 ResourceArn=r['ReadinessCheckArn'],
724 TagKeys=keys)
727@ReadinessCheck.action_registry.register('mark-for-op')
728class MarkForOpReadinessCheck(tags.TagDelayedAction):
730 def get_client(self):
731 return self.manager.get_client()
734@ReadinessCheck.filter_registry.register('marked-for-op')
735class MarkedForOpReadinessCheck(tags.TagActionFilter):
737 def get_client(self):
738 return self.manager.get_client()
741@ReadinessCheck.filter_registry.register('cross-account')
742class ReadinessCheckCrossAccount(CrossAccountAccessFilter):
744 schema = type_schema(
745 'cross-account',
746 # white list accounts
747 whitelist_from=ValuesFrom.schema,
748 whitelist={'type': 'array', 'items': {'type': 'string'}})
749 permissions = ('route53-recovery-readiness:ListCrossAccountAuthorizations',)
751 def process(self, resources, event=None):
752 allowed_accounts = set(self.get_accounts())
753 results, account_ids, found = [], [], False
755 paginator = self.manager.get_client().get_paginator('list_cross_account_authorizations')
756 paginator.PAGE_ITERATOR_CLASS = RetryPageIterator
757 arns = paginator.paginate().build_full_result()["CrossAccountAuthorizations"]
759 for arn in arns:
760 account_id = arn.split(':', 5)[4]
761 if account_id not in allowed_accounts:
762 account_ids.append(account_id)
763 found = True
764 if (found):
765 for r in resources:
766 r['c7n:CrossAccountViolations'] = account_ids
767 results.append(r)
769 return results
773class DescribeCluster(query.DescribeSource):
774 def augment(self, clusters):
775 for r in clusters:
776 Tags = self.manager.retry(
777 self.manager.get_client().list_tags_for_resource,
778 ResourceArn=r['ClusterArn'])['Tags']
779 r['Tags'] = [{'Key': k, "Value": v} for k, v in Tags.items()]
780 return clusters
783@resources.register('recovery-cluster')
784class RecoveryCluster(QueryResourceManager):
786 class resource_type(TypeInfo):
787 service = 'route53-recovery-control-config'
788 arn_type = 'cluster'
789 enum_spec = ('list_clusters', 'Clusters', None)
790 name = id = 'ClusterArn'
791 global_resource = True
792 config_type = cfn_type = "AWS::Route53RecoveryControl::Cluster"
794 source_mapping = {
795 'describe': DescribeCluster,
796 'config': query.ConfigSource
797 }
799 def get_client(self):
800 return local_session(self.session_factory) \
801 .client('route53-recovery-control-config', region_name=ARC_REGION)
805@RecoveryCluster.action_registry.register('tag')
806class RecoveryClusterAddTag(Tag):
807 """Adds tags to a cluster
809 :example:
811 .. code-block:: yaml
813 policies:
814 - name: recovery-cluster-tag
815 resource: recovery-cluster
816 filters:
817 - "tag:DesiredTag": absent
818 actions:
819 - type: tag
820 key: DesiredTag
821 value: DesiredValue
822 """
823 permissions = ('route53-recovery-control-config:TagResource',)
825 def get_client(self):
826 return self.manager.get_client()
828 def process_resource_set(self, client, clusters, tags):
829 Tags = {r['Key']: r['Value'] for r in tags}
830 for r in clusters:
831 client.tag_resource(
832 ResourceArn=r['ClusterArn'],
833 Tags=Tags)
836@RecoveryCluster.action_registry.register('remove-tag')
837class RecoveryClusterRemoveTag(RemoveTag):
838 """Remove tags from a cluster
840 :example:
844 .. code-block:: yaml
846 policies:
847 - name: recovery-cluster-remove-tag
848 resource: recovery-cluster
849 filters:
850 - "tag:ExpiredTag": present
851 actions:
852 - type: remove-tag
853 tags: ['ExpiredTag']
854 """
855 permissions = ('route53-recovery-control-config:UntagResource',)
857 def get_client(self):
858 return self.manager.get_client()
860 def process_resource_set(self, client, clusters, keys):
861 for r in clusters:
862 client.untag_resource(
863 ResourceArn=r['ClusterArn'],
864 TagKeys=keys)
867@query.sources.register('describe-control-panel')
868class DescribeControlPanel(query.ChildDescribeSource):
870 def __init__(self, manager):
871 self.manager = manager
872 self.query = query.ChildResourceQuery(
873 self.manager.session_factory, self.manager)
875 def augment(self, controlpanels):
876 for r in controlpanels:
877 Tags = self.manager.retry(
878 self.manager.get_client().list_tags_for_resource,
879 ResourceArn=r['ControlPanelArn'])['Tags']
880 r['Tags'] = [{'Key': k, "Value": v} for k, v in Tags.items()]
881 return controlpanels
884@resources.register('recovery-control-panel')
885class ControlPanel(query.ChildResourceManager):
887 class resource_type(query.TypeInfo):
888 service = 'route53-recovery-control-config'
889 arn_type = 'controlpanel'
890 parent_spec = ('recovery-cluster', 'ClusterArn', None)
891 enum_spec = ('list_control_panels', 'ControlPanels', None)
892 name = id = 'ControlPanelArn'
893 config_type = cfn_type = "AWS::Route53RecoveryControl::ControlPanel"
894 global_resource = True
896 child_source = 'describe'
897 source_mapping = {
898 'describe': DescribeControlPanel,
899 'config': query.ConfigSource
900 }
902 def get_client(self):
903 return local_session(self.session_factory) \
904 .client('route53-recovery-control-config', region_name=ARC_REGION)
907@ControlPanel.action_registry.register('tag')
908class ControlPanelAddTag(Tag):
909 """Adds tags to a control panel
911 :example:
913 .. code-block:: yaml
915 policies:
916 - name: control-panel-tag
917 resource: recovery-control-panel
918 filters:
919 - "tag:DesiredTag": absent
920 actions:
921 - type: tag
922 key: DesiredTag
923 value: DesiredValue
924 """
925 permissions = ('route53-recovery-control-config:TagResource',)
927 def get_client(self):
928 return self.manager.get_client()
930 def process_resource_set(self, client, controlpanels, tags):
931 Tags = {r['Key']: r['Value'] for r in tags}
932 for r in controlpanels:
933 client.tag_resource(
934 ResourceArn=r['ControlPanelArn'],
935 Tags=Tags)
938@ControlPanel.action_registry.register('remove-tag')
939class ControlPanelRemoveTag(RemoveTag):
940 """Remove tags from a control panel
942 :example:
944 .. code-block:: yaml
946 policies:
947 - name: control-panel-remove-tag
948 resource: recovery-control-panel
949 filters:
950 - "tag:ExpiredTag": present
951 actions:
952 - type: remove-tag
953 tags: ['ExpiredTag']
954 """
955 permissions = ('route53-recovery-control-config:UntagResource',)
957 def get_client(self):
958 return self.manager.get_client()
960 def process_resource_set(self, client, control_panels, keys):
961 for r in control_panels:
962 client.untag_resource(
963 ResourceArn=r['ControlPanelArn'],
964 TagKeys=keys)
967@ControlPanel.filter_registry.register('safety-rule')
968class SafeRule(ListItemFilter):
969 """Filter the safety rules (the assertion rules and gating rules)
970 that you’ve defined for the routing controls in a control panel.
973 :example:
975 find a recovery control panel with at least two deployed assertion safety rules
976 with a mininum of 30m wait period.
978 .. code-block:: yaml
980 policies:
981 - name: check-safety
982 resource: aws.recovery-control-panel
983 filters:
984 - type: safety-rule
985 count: 2
986 count_op: gte
987 attrs:
988 - Type: ASSERTION
989 - Status: Deployed
990 - type: value
991 key: WaitPeriodMs
992 op: gte
993 value: 30
994 """
995 permissions = ('route53-recovery-control-config:ListSafetyRules',)
996 schema = type_schema(
997 'safety-rule',
998 attrs={'$ref': '#/definitions/filters_common/list_item_attrs'},
999 count={'type': 'number'},
1000 count_op={'$ref': '#/definitions/filters_common/comparison_operators'}
1001 )
1003 _client = None
1005 def get_client(self):
1006 if self._client:
1007 return self._client
1008 self._client = self.manager.get_client()
1009 return self._client
1011 def get_item_values(self, resource):
1012 paginator = self.get_client().get_paginator('list_safety_rules')
1013 paginator.PAGE_ITERATOR_CLASS = RetryPageIterator
1014 rules = []
1015 results = paginator.paginate(
1016 ControlPanelArn=resource['ControlPanelArn']).build_full_result().get('SafetyRules', [])
1017 for block in results:
1018 for rule_type, type_rule in block.items():
1019 type_rule['Type'] = rule_type
1020 rules.append(type_rule)
1021 return rules