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