Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/c7n/resources/ec2.py: 37%
1038 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 base64
4import itertools
5import operator
6import random
7import re
8import zlib
9from typing import List
10from distutils.version import LooseVersion
12import botocore
13from botocore.exceptions import ClientError
14from dateutil.parser import parse
15from concurrent.futures import as_completed
17from c7n.actions import (
18 ActionRegistry, BaseAction, ModifyVpcSecurityGroupsAction, AutoscalingBase
19)
21from c7n.exceptions import PolicyValidationError
22from c7n.filters import (
23 FilterRegistry, AgeFilter, ValueFilter, Filter
24)
25from c7n.filters.offhours import OffHour, OnHour
26import c7n.filters.vpc as net_filters
28from c7n.manager import resources
29from c7n import query, utils
30from c7n.tags import coalesce_copy_user_tags
31from c7n.utils import type_schema, filter_empty, jmespath_search, jmespath_compile
33from c7n.resources.iam import CheckPermissions, SpecificIamProfileManagedPolicy
34from c7n.resources.securityhub import PostFinding
36RE_ERROR_INSTANCE_ID = re.compile("'(?P<instance_id>i-.*?)'")
38filters = FilterRegistry('ec2.filters')
39actions = ActionRegistry('ec2.actions')
42class DescribeEC2(query.DescribeSource):
44 def get_query_params(self, query_params):
45 queries = QueryFilter.parse(self.manager.data.get('query', []))
46 qf = []
47 for q in queries:
48 qd = q.query()
49 found = False
50 for f in qf:
51 if qd['Name'] == f['Name']:
52 f['Values'].extend(qd['Values'])
53 found = True
54 if not found:
55 qf.append(qd)
56 query_params = query_params or {}
57 query_params['Filters'] = qf
58 return query_params
60 def augment(self, resources):
61 """EC2 API and AWOL Tags
63 While ec2 api generally returns tags when doing describe_x on for
64 various resources, it may also silently fail to do so unless a tag
65 is used as a filter.
67 See footnote on for official documentation.
68 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#Using_Tags_CLI
70 Apriori we may be using custodian to ensure tags (including
71 name), so there isn't a good default to ensure that we will
72 always get tags from describe_x calls.
73 """
74 # First if we're in event based lambda go ahead and skip this,
75 # tags can't be trusted in ec2 instances immediately post creation.
76 if not resources or self.manager.data.get(
77 'mode', {}).get('type', '') in (
78 'cloudtrail', 'ec2-instance-state'):
79 return resources
81 # AWOL detector, so we don't make extraneous api calls.
82 resource_count = len(resources)
83 search_count = min(int(resource_count % 0.05) + 1, 5)
84 if search_count > resource_count:
85 search_count = resource_count
86 found = False
87 for r in random.sample(resources, search_count):
88 if 'Tags' in r:
89 found = True
90 break
92 if found:
93 return resources
95 # Okay go and do the tag lookup
96 client = utils.local_session(self.manager.session_factory).client('ec2')
97 tag_set = self.manager.retry(
98 client.describe_tags,
99 Filters=[{'Name': 'resource-type',
100 'Values': ['instance']}])['Tags']
101 resource_tags = {}
102 for t in tag_set:
103 t.pop('ResourceType')
104 rid = t.pop('ResourceId')
105 resource_tags.setdefault(rid, []).append(t)
107 m = self.manager.get_model()
108 for r in resources:
109 r['Tags'] = resource_tags.get(r[m.id], [])
110 return resources
113@resources.register('ec2')
114class EC2(query.QueryResourceManager):
116 class resource_type(query.TypeInfo):
117 service = 'ec2'
118 arn_type = 'instance'
119 enum_spec = ('describe_instances', 'Reservations[].Instances[]', None)
120 id = 'InstanceId'
121 filter_name = 'InstanceIds'
122 filter_type = 'list'
123 name = 'PublicDnsName'
124 date = 'LaunchTime'
125 dimension = 'InstanceId'
126 cfn_type = config_type = "AWS::EC2::Instance"
127 id_prefix = 'i-'
129 default_report_fields = (
130 'CustodianDate',
131 'InstanceId',
132 'tag:Name',
133 'InstanceType',
134 'LaunchTime',
135 'VpcId',
136 'PrivateIpAddress',
137 )
139 filter_registry = filters
140 action_registry = actions
142 # if we have to do a fallback scenario where tags don't come in describe
143 permissions = ('ec2:DescribeTags',)
144 source_mapping = {
145 'describe': DescribeEC2,
146 'config': query.ConfigSource
147 }
150@filters.register('security-group')
151class SecurityGroupFilter(net_filters.SecurityGroupFilter):
153 RelatedIdsExpression = "NetworkInterfaces[].Groups[].GroupId"
156@filters.register('subnet')
157class SubnetFilter(net_filters.SubnetFilter):
159 RelatedIdsExpression = "NetworkInterfaces[].SubnetId"
162@filters.register('vpc')
163class VpcFilter(net_filters.VpcFilter):
165 RelatedIdsExpression = "VpcId"
168@filters.register('check-permissions')
169class ComputePermissions(CheckPermissions):
171 def get_iam_arns(self, resources):
172 profile_arn_map = {
173 r['IamInstanceProfile']['Arn']: r['IamInstanceProfile']['Id']
174 for r in resources if 'IamInstanceProfile' in r}
176 # py2 compat on dict ordering
177 profile_arns = list(profile_arn_map.items())
178 profile_role_map = {
179 arn: profile['Roles'][0]['Arn']
180 for arn, profile in zip(
181 [p[0] for p in profile_arns],
182 self.manager.get_resource_manager(
183 'iam-profile').get_resources(
184 [p[0].split('/', 1)[-1] for p in profile_arns]))}
185 return [
186 profile_role_map.get(r.get('IamInstanceProfile', {}).get('Arn'))
187 for r in resources]
190@filters.register('state-age')
191class StateTransitionAge(AgeFilter):
192 """Age an instance has been in the given state.
194 .. code-block:: yaml
196 policies:
197 - name: ec2-state-running-7-days
198 resource: ec2
199 filters:
200 - type: state-age
201 op: ge
202 days: 7
203 """
204 RE_PARSE_AGE = re.compile(r"\(.*?\)")
206 # this filter doesn't use date_attribute, but needs to define it
207 # to pass AgeFilter's validate method
208 date_attribute = "dummy"
210 schema = type_schema(
211 'state-age',
212 op={'$ref': '#/definitions/filters_common/comparison_operators'},
213 days={'type': 'number'})
215 def get_resource_date(self, i):
216 v = i.get('StateTransitionReason')
217 if not v:
218 return None
219 dates = self.RE_PARSE_AGE.findall(v)
220 if dates:
221 return parse(dates[0][1:-1])
222 return None
225@filters.register('ebs')
226class AttachedVolume(ValueFilter):
227 """EC2 instances with EBS backed volume
229 Filters EC2 instances with EBS backed storage devices (non ephemeral)
231 :Example:
233 .. code-block:: yaml
235 policies:
236 - name: ec2-encrypted-ebs-volumes
237 resource: ec2
238 filters:
239 - type: ebs
240 key: Encrypted
241 value: true
242 """
244 schema = type_schema(
245 'ebs', rinherit=ValueFilter.schema,
246 **{'operator': {'enum': ['and', 'or']},
247 'skip-devices': {'type': 'array', 'items': {'type': 'string'}}})
248 schema_alias = False
250 def get_permissions(self):
251 return self.manager.get_resource_manager('ebs').get_permissions()
253 def process(self, resources, event=None):
254 self.volume_map = self.get_volume_mapping(resources)
255 self.skip = self.data.get('skip-devices', [])
256 self.operator = self.data.get(
257 'operator', 'or') == 'or' and any or all
258 return list(filter(self, resources))
260 def get_volume_mapping(self, resources):
261 volume_map = {}
262 manager = self.manager.get_resource_manager('ebs')
263 for instance_set in utils.chunks(resources, 200):
264 volume_ids = []
265 for i in instance_set:
266 for bd in i.get('BlockDeviceMappings', ()):
267 if 'Ebs' not in bd:
268 continue
269 volume_ids.append(bd['Ebs']['VolumeId'])
270 for v in manager.get_resources(volume_ids):
271 if not v['Attachments']:
272 continue
273 volume_map.setdefault(
274 v['Attachments'][0]['InstanceId'], []).append(v)
275 return volume_map
277 def __call__(self, i):
278 volumes = self.volume_map.get(i['InstanceId'])
279 if not volumes:
280 return False
281 if self.skip:
282 for v in list(volumes):
283 for a in v.get('Attachments', []):
284 if a['Device'] in self.skip:
285 volumes.remove(v)
286 return self.operator(map(self.match, volumes))
289@filters.register('stop-protected')
290class DisableApiStop(Filter):
291 """EC2 instances with ``disableApiStop`` attribute set
293 Filters EC2 instances with ``disableApiStop`` attribute set to true.
295 :Example:
297 .. code-block:: yaml
299 policies:
300 - name: stop-protection-enabled
301 resource: ec2
302 filters:
303 - type: stop-protected
305 :Example:
307 .. code-block:: yaml
309 policies:
310 - name: stop-protection-NOT-enabled
311 resource: ec2
312 filters:
313 - not:
314 - type: stop-protected
315 """
317 schema = type_schema('stop-protected')
318 permissions = ('ec2:DescribeInstanceAttribute',)
320 def process(self, resources: List[dict], event=None) -> List[dict]:
321 client = utils.local_session(
322 self.manager.session_factory).client('ec2')
323 return [r for r in resources
324 if self._is_stop_protection_enabled(client, r)]
326 def _is_stop_protection_enabled(self, client, instance: dict) -> bool:
327 attr_val = self.manager.retry(
328 client.describe_instance_attribute,
329 Attribute='disableApiStop',
330 InstanceId=instance['InstanceId']
331 )
332 return attr_val['DisableApiStop']['Value']
334 def validate(self) -> None:
335 botocore_min_version = '1.26.7'
337 if LooseVersion(botocore.__version__) < LooseVersion(botocore_min_version):
338 raise PolicyValidationError(
339 "'stop-protected' filter requires botocore version "
340 f'{botocore_min_version} or above. '
341 f'Installed version is {botocore.__version__}.'
342 )
345@filters.register('termination-protected')
346class DisableApiTermination(Filter):
347 """EC2 instances with ``disableApiTermination`` attribute set
349 Filters EC2 instances with ``disableApiTermination`` attribute set to true.
351 :Example:
353 .. code-block:: yaml
355 policies:
356 - name: termination-protection-enabled
357 resource: ec2
358 filters:
359 - type: termination-protected
361 :Example:
363 .. code-block:: yaml
365 policies:
366 - name: termination-protection-NOT-enabled
367 resource: ec2
368 filters:
369 - not:
370 - type: termination-protected
371 """
373 schema = type_schema('termination-protected')
374 permissions = ('ec2:DescribeInstanceAttribute',)
376 def get_permissions(self):
377 perms = list(self.permissions)
378 perms.extend(self.manager.get_permissions())
379 return perms
381 def process(self, resources, event=None):
382 client = utils.local_session(
383 self.manager.session_factory).client('ec2')
384 return [r for r in resources
385 if self.is_termination_protection_enabled(client, r)]
387 def is_termination_protection_enabled(self, client, inst):
388 attr_val = self.manager.retry(
389 client.describe_instance_attribute,
390 Attribute='disableApiTermination',
391 InstanceId=inst['InstanceId']
392 )
393 return attr_val['DisableApiTermination']['Value']
396class InstanceImageBase:
398 def prefetch_instance_images(self, instances):
399 image_ids = [i['ImageId'] for i in instances if 'c7n:instance-image' not in i]
400 self.image_map = self.get_local_image_mapping(image_ids)
402 def get_base_image_mapping(self):
403 return {i['ImageId']: i for i in
404 self.manager.get_resource_manager('ami').resources()}
406 def get_instance_image(self, instance):
407 image = instance.get('c7n:instance-image', None)
408 if not image:
409 image = instance['c7n:instance-image'] = self.image_map.get(instance['ImageId'], None)
410 return image
412 def get_local_image_mapping(self, image_ids):
413 base_image_map = self.get_base_image_mapping()
414 resources = {i: base_image_map[i] for i in image_ids if i in base_image_map}
415 missing = list(set(image_ids) - set(resources.keys()))
416 if missing:
417 loaded = self.manager.get_resource_manager('ami').get_resources(missing, False)
418 resources.update({image['ImageId']: image for image in loaded})
419 return resources
422@filters.register('image-age')
423class ImageAge(AgeFilter, InstanceImageBase):
424 """EC2 AMI age filter
426 Filters EC2 instances based on the age of their AMI image (in days)
428 :Example:
430 .. code-block:: yaml
432 policies:
433 - name: ec2-ancient-ami
434 resource: ec2
435 filters:
436 - type: image-age
437 op: ge
438 days: 90
439 """
441 date_attribute = "CreationDate"
443 schema = type_schema(
444 'image-age',
445 op={'$ref': '#/definitions/filters_common/comparison_operators'},
446 days={'type': 'number'})
448 def get_permissions(self):
449 return self.manager.get_resource_manager('ami').get_permissions()
451 def process(self, resources, event=None):
452 self.prefetch_instance_images(resources)
453 return super(ImageAge, self).process(resources, event)
455 def get_resource_date(self, i):
456 image = self.get_instance_image(i)
457 if image:
458 return parse(image['CreationDate'])
459 else:
460 return parse("2000-01-01T01:01:01.000Z")
463@filters.register('image')
464class InstanceImage(ValueFilter, InstanceImageBase):
466 schema = type_schema('image', rinherit=ValueFilter.schema)
467 schema_alias = False
469 def get_permissions(self):
470 return self.manager.get_resource_manager('ami').get_permissions()
472 def process(self, resources, event=None):
473 self.prefetch_instance_images(resources)
474 return super(InstanceImage, self).process(resources, event)
476 def __call__(self, i):
477 image = self.get_instance_image(i)
478 # Finally, if we have no image...
479 if not image:
480 self.log.warning(
481 "Could not locate image for instance:%s ami:%s" % (
482 i['InstanceId'], i["ImageId"]))
483 # Match instead on empty skeleton?
484 return False
485 return self.match(image)
488@filters.register('offhour')
489class InstanceOffHour(OffHour):
490 """Custodian OffHour filter
492 Filters running EC2 instances with the intent to stop at a given hour of
493 the day. A list of days to excluded can be included as a list of strings
494 with the format YYYY-MM-DD. Alternatively, the list (using the same syntax)
495 can be taken from a specified url.
497 Note: You can disable filtering of only running instances by setting
498 `state-filter: false`
500 :Example:
502 .. code-block:: yaml
504 policies:
505 - name: offhour-evening-stop
506 resource: ec2
507 filters:
508 - type: offhour
509 tag: custodian_downtime
510 default_tz: et
511 offhour: 20
512 actions:
513 - stop
515 - name: offhour-evening-stop-skip-holidays
516 resource: ec2
517 filters:
518 - type: offhour
519 tag: custodian_downtime
520 default_tz: et
521 offhour: 20
522 skip-days: ['2017-12-25']
523 actions:
524 - stop
526 - name: offhour-evening-stop-skip-holidays-from
527 resource: ec2
528 filters:
529 - type: offhour
530 tag: custodian_downtime
531 default_tz: et
532 offhour: 20
533 skip-days-from:
534 expr: 0
535 format: csv
536 url: 's3://location/holidays.csv'
537 actions:
538 - stop
539 """
541 schema = type_schema(
542 'offhour', rinherit=OffHour.schema,
543 **{'state-filter': {'type': 'boolean'}})
544 schema_alias = False
546 valid_origin_states = ('running',)
548 def process(self, resources, event=None):
549 if self.data.get('state-filter', True):
550 return super(InstanceOffHour, self).process(
551 self.filter_resources(resources, 'State.Name', self.valid_origin_states))
552 else:
553 return super(InstanceOffHour, self).process(resources)
556@filters.register('network-location')
557class EC2NetworkLocation(net_filters.NetworkLocation):
559 valid_origin_states = ('pending', 'running', 'shutting-down', 'stopping',
560 'stopped')
562 def process(self, resources, event=None):
563 resources = self.filter_resources(resources, 'State.Name', self.valid_origin_states)
564 if not resources:
565 return []
566 return super(EC2NetworkLocation, self).process(resources)
569@filters.register('onhour')
570class InstanceOnHour(OnHour):
571 """Custodian OnHour filter
573 Filters stopped EC2 instances with the intent to start at a given hour of
574 the day. A list of days to excluded can be included as a list of strings
575 with the format YYYY-MM-DD. Alternatively, the list (using the same syntax)
576 can be taken from a specified url.
578 Note: You can disable filtering of only stopped instances by setting
579 `state-filter: false`
581 :Example:
583 .. code-block:: yaml
585 policies:
586 - name: onhour-morning-start
587 resource: ec2
588 filters:
589 - type: onhour
590 tag: custodian_downtime
591 default_tz: et
592 onhour: 6
593 actions:
594 - start
596 - name: onhour-morning-start-skip-holidays
597 resource: ec2
598 filters:
599 - type: onhour
600 tag: custodian_downtime
601 default_tz: et
602 onhour: 6
603 skip-days: ['2017-12-25']
604 actions:
605 - start
607 - name: onhour-morning-start-skip-holidays-from
608 resource: ec2
609 filters:
610 - type: onhour
611 tag: custodian_downtime
612 default_tz: et
613 onhour: 6
614 skip-days-from:
615 expr: 0
616 format: csv
617 url: 's3://location/holidays.csv'
618 actions:
619 - start
620 """
622 schema = type_schema(
623 'onhour', rinherit=OnHour.schema,
624 **{'state-filter': {'type': 'boolean'}})
625 schema_alias = False
627 valid_origin_states = ('stopped',)
629 def process(self, resources, event=None):
630 if self.data.get('state-filter', True):
631 return super(InstanceOnHour, self).process(
632 self.filter_resources(resources, 'State.Name', self.valid_origin_states))
633 else:
634 return super(InstanceOnHour, self).process(resources)
637@filters.register('ephemeral')
638class EphemeralInstanceFilter(Filter):
639 """EC2 instances with ephemeral storage
641 Filters EC2 instances that have ephemeral storage (an instance-store backed
642 root device)
644 :Example:
646 .. code-block:: yaml
648 policies:
649 - name: ec2-ephemeral-instances
650 resource: ec2
651 filters:
652 - type: ephemeral
654 http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html
655 """
657 schema = type_schema('ephemeral')
659 def __call__(self, i):
660 return self.is_ephemeral(i)
662 @staticmethod
663 def is_ephemeral(i):
664 for bd in i.get('BlockDeviceMappings', []):
665 if bd['DeviceName'] in ('/dev/sda1', '/dev/xvda', 'xvda'):
666 if 'Ebs' in bd:
667 return False
668 return True
669 return True
672@filters.register('instance-uptime')
673class UpTimeFilter(AgeFilter):
675 date_attribute = "LaunchTime"
677 schema = type_schema(
678 'instance-uptime',
679 op={'$ref': '#/definitions/filters_common/comparison_operators'},
680 days={'type': 'number'})
683@filters.register('instance-age')
684class InstanceAgeFilter(AgeFilter):
685 """Filters instances based on their age (in days)
687 :Example:
689 .. code-block:: yaml
691 policies:
692 - name: ec2-30-days-plus
693 resource: ec2
694 filters:
695 - type: instance-age
696 op: ge
697 days: 30
698 """
700 date_attribute = "LaunchTime"
701 ebs_key_func = operator.itemgetter('AttachTime')
703 schema = type_schema(
704 'instance-age',
705 op={'$ref': '#/definitions/filters_common/comparison_operators'},
706 days={'type': 'number'},
707 hours={'type': 'number'},
708 minutes={'type': 'number'})
710 def get_resource_date(self, i):
711 # LaunchTime is basically how long has the instance
712 # been on, use the oldest ebs vol attach time
713 ebs_vols = [
714 block['Ebs'] for block in i['BlockDeviceMappings']
715 if 'Ebs' in block]
716 if not ebs_vols:
717 # Fall back to using age attribute (ephemeral instances)
718 return super(InstanceAgeFilter, self).get_resource_date(i)
719 # Lexographical sort on date
720 ebs_vols = sorted(ebs_vols, key=self.ebs_key_func)
721 return ebs_vols[0]['AttachTime']
724@filters.register('default-vpc')
725class DefaultVpc(net_filters.DefaultVpcBase):
726 """ Matches if an ec2 database is in the default vpc
727 """
729 schema = type_schema('default-vpc')
731 def __call__(self, ec2):
732 return ec2.get('VpcId') and self.match(ec2.get('VpcId')) or False
735def deserialize_user_data(user_data):
736 data = base64.b64decode(user_data)
737 # try raw and compressed
738 try:
739 return data.decode('utf8')
740 except UnicodeDecodeError:
741 return zlib.decompress(data, 16).decode('utf8')
744@filters.register('user-data')
745class UserData(ValueFilter):
746 """Filter on EC2 instances which have matching userdata.
747 Note: It is highly recommended to use regexes with the ?sm flags, since Custodian
748 uses re.match() and userdata spans multiple lines.
750 :example:
752 .. code-block:: yaml
754 policies:
755 - name: ec2_userdata_stop
756 resource: ec2
757 filters:
758 - type: user-data
759 op: regex
760 value: (?smi).*password=
761 actions:
762 - stop
763 """
765 schema = type_schema('user-data', rinherit=ValueFilter.schema)
766 schema_alias = False
767 batch_size = 50
768 annotation = 'c7n:user-data'
769 permissions = ('ec2:DescribeInstanceAttribute',)
771 def __init__(self, data, manager):
772 super(UserData, self).__init__(data, manager)
773 self.data['key'] = '"c7n:user-data"'
775 def process(self, resources, event=None):
776 client = utils.local_session(self.manager.session_factory).client('ec2')
777 results = []
778 with self.executor_factory(max_workers=3) as w:
779 futures = {}
780 for instance_set in utils.chunks(resources, self.batch_size):
781 futures[w.submit(
782 self.process_instance_set,
783 client, instance_set)] = instance_set
785 for f in as_completed(futures):
786 if f.exception():
787 self.log.error(
788 "Error processing userdata on instance set %s", f.exception())
789 results.extend(f.result())
790 return results
792 def process_instance_set(self, client, resources):
793 results = []
794 for r in resources:
795 if self.annotation not in r:
796 try:
797 result = client.describe_instance_attribute(
798 Attribute='userData',
799 InstanceId=r['InstanceId'])
800 except ClientError as e:
801 if e.response['Error']['Code'] == 'InvalidInstanceId.NotFound':
802 continue
803 if 'Value' not in result['UserData']:
804 r[self.annotation] = None
805 else:
806 r[self.annotation] = deserialize_user_data(
807 result['UserData']['Value'])
808 if self.match(r):
809 results.append(r)
810 return results
813@filters.register('singleton')
814class SingletonFilter(Filter):
815 """EC2 instances without autoscaling or a recover alarm
817 Filters EC2 instances that are not members of an autoscaling group
818 and do not have Cloudwatch recover alarms.
820 :Example:
822 .. code-block:: yaml
824 policies:
825 - name: ec2-recover-instances
826 resource: ec2
827 filters:
828 - singleton
829 actions:
830 - type: tag
831 key: problem
832 value: instance is not resilient
834 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-recover.html
835 """
837 schema = type_schema('singleton')
839 permissions = ('cloudwatch:DescribeAlarmsForMetric',)
841 valid_origin_states = ('running', 'stopped', 'pending', 'stopping')
843 in_asg = ValueFilter({
844 'key': 'tag:aws:autoscaling:groupName',
845 'value': 'not-null'}).validate()
847 def process(self, instances, event=None):
848 return super(SingletonFilter, self).process(
849 self.filter_resources(instances, 'State.Name', self.valid_origin_states))
851 def __call__(self, i):
852 if self.in_asg(i):
853 return False
854 else:
855 return not self.has_recover_alarm(i)
857 def has_recover_alarm(self, i):
858 client = utils.local_session(self.manager.session_factory).client('cloudwatch')
859 alarms = client.describe_alarms_for_metric(
860 MetricName='StatusCheckFailed_System',
861 Namespace='AWS/EC2',
862 Dimensions=[
863 {
864 'Name': 'InstanceId',
865 'Value': i['InstanceId']
866 }
867 ]
868 )
870 for i in alarms['MetricAlarms']:
871 for a in i['AlarmActions']:
872 if (
873 a.startswith('arn:aws:automate:') and
874 a.endswith(':ec2:recover')
875 ):
876 return True
878 return False
881@EC2.filter_registry.register('ssm')
882class SsmStatus(ValueFilter):
883 """Filter ec2 instances by their ssm status information.
885 :Example:
887 Find ubuntu 18.04 instances are active with ssm.
889 .. code-block:: yaml
891 policies:
892 - name: ec2-ssm-check
893 resource: ec2
894 filters:
895 - type: ssm
896 key: PingStatus
897 value: Online
898 - type: ssm
899 key: PlatformName
900 value: Ubuntu
901 - type: ssm
902 key: PlatformVersion
903 value: 18.04
904 """
905 schema = type_schema('ssm', rinherit=ValueFilter.schema)
906 schema_alias = False
907 permissions = ('ssm:DescribeInstanceInformation',)
908 annotation = 'c7n:SsmState'
910 def process(self, resources, event=None):
911 client = utils.local_session(self.manager.session_factory).client('ssm')
912 results = []
913 for resource_set in utils.chunks(
914 [r for r in resources if self.annotation not in r], 50):
915 self.process_resource_set(client, resource_set)
916 for r in resources:
917 if self.match(r[self.annotation]):
918 results.append(r)
919 return results
921 def process_resource_set(self, client, resources):
922 instance_ids = [i['InstanceId'] for i in resources]
923 info_map = {
924 info['InstanceId']: info for info in
925 client.describe_instance_information(
926 Filters=[{'Key': 'InstanceIds', 'Values': instance_ids}]).get(
927 'InstanceInformationList', [])}
928 for r in resources:
929 r[self.annotation] = info_map.get(r['InstanceId'], {})
932@EC2.filter_registry.register('ssm-compliance')
933class SsmCompliance(Filter):
934 """Filter ec2 instances by their ssm compliance status.
936 :Example:
938 Find non-compliant ec2 instances.
940 .. code-block:: yaml
942 policies:
943 - name: ec2-ssm-compliance
944 resource: ec2
945 filters:
946 - type: ssm-compliance
947 compliance_types:
948 - Association
949 - Patch
950 severity:
951 - CRITICAL
952 - HIGH
953 - MEDIUM
954 - LOW
955 - UNSPECIFIED
956 states:
957 - NON_COMPLIANT
958 eval_filters:
959 - type: value
960 key: ExecutionSummary.ExecutionTime
961 value_type: age
962 value: 30
963 op: less-than
964 """
965 schema = type_schema(
966 'ssm-compliance',
967 **{'required': ['compliance_types'],
968 'compliance_types': {'type': 'array', 'items': {'type': 'string'}},
969 'severity': {'type': 'array', 'items': {'type': 'string'}},
970 'op': {'enum': ['or', 'and']},
971 'eval_filters': {'type': 'array', 'items': {
972 'oneOf': [
973 {'$ref': '#/definitions/filters/valuekv'},
974 {'$ref': '#/definitions/filters/value'}]}},
975 'states': {'type': 'array',
976 'default': ['NON_COMPLIANT'],
977 'items': {
978 'enum': [
979 'COMPLIANT',
980 'NON_COMPLIANT'
981 ]}}})
982 permissions = ('ssm:ListResourceComplianceSummaries',)
983 annotation = 'c7n:ssm-compliance'
985 def process(self, resources, event=None):
986 op = self.data.get('op', 'or') == 'or' and any or all
987 eval_filters = []
988 for f in self.data.get('eval_filters', ()):
989 vf = ValueFilter(f)
990 vf.annotate = False
991 eval_filters.append(vf)
993 client = utils.local_session(self.manager.session_factory).client('ssm')
994 filters = [
995 {
996 'Key': 'Status',
997 'Values': self.data['states'],
998 'Type': 'EQUAL'
999 },
1000 {
1001 'Key': 'ComplianceType',
1002 'Values': self.data['compliance_types'],
1003 'Type': 'EQUAL'
1004 }
1005 ]
1006 severity = self.data.get('severity')
1007 if severity:
1008 filters.append(
1009 {
1010 'Key': 'OverallSeverity',
1011 'Values': severity,
1012 'Type': 'EQUAL'
1013 })
1015 resource_map = {}
1016 pager = client.get_paginator('list_resource_compliance_summaries')
1017 for page in pager.paginate(Filters=filters):
1018 items = page['ResourceComplianceSummaryItems']
1019 for i in items:
1020 if not eval_filters:
1021 resource_map.setdefault(
1022 i['ResourceId'], []).append(i)
1023 continue
1024 if op([f.match(i) for f in eval_filters]):
1025 resource_map.setdefault(
1026 i['ResourceId'], []).append(i)
1028 results = []
1029 for r in resources:
1030 result = resource_map.get(r['InstanceId'])
1031 if result:
1032 r[self.annotation] = result
1033 results.append(r)
1035 return results
1038@actions.register('set-monitoring')
1039class MonitorInstances(BaseAction):
1040 """Action on EC2 Instances to enable/disable detailed monitoring
1042 The different states of detailed monitoring status are :
1043 'disabled'|'disabling'|'enabled'|'pending'
1044 (https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.describe_instances)
1046 :Example:
1048 .. code-block:: yaml
1050 policies:
1051 - name: ec2-detailed-monitoring-activation
1052 resource: ec2
1053 filters:
1054 - Monitoring.State: disabled
1055 actions:
1056 - type: set-monitoring
1057 state: enable
1059 References
1061 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-cloudwatch-new.html
1062 """
1063 schema = type_schema('set-monitoring',
1064 **{'state': {'enum': ['enable', 'disable']}})
1065 permissions = ('ec2:MonitorInstances', 'ec2:UnmonitorInstances')
1067 def process(self, resources, event=None):
1068 client = utils.local_session(
1069 self.manager.session_factory).client('ec2')
1070 actions = {
1071 'enable': self.enable_monitoring,
1072 'disable': self.disable_monitoring
1073 }
1074 for instances_set in utils.chunks(resources, 20):
1075 actions[self.data.get('state')](client, instances_set)
1077 def enable_monitoring(self, client, resources):
1078 try:
1079 client.monitor_instances(
1080 InstanceIds=[inst['InstanceId'] for inst in resources]
1081 )
1082 except ClientError as e:
1083 if e.response['Error']['Code'] != 'InvalidInstanceId.NotFound':
1084 raise
1086 def disable_monitoring(self, client, resources):
1087 try:
1088 client.unmonitor_instances(
1089 InstanceIds=[inst['InstanceId'] for inst in resources]
1090 )
1091 except ClientError as e:
1092 if e.response['Error']['Code'] != 'InvalidInstanceId.NotFound':
1093 raise
1096@EC2.action_registry.register('set-metadata-access')
1097class SetMetadataServerAccess(BaseAction):
1098 """Set instance metadata server access for an instance.
1100 :example:
1102 Require instances to use IMDSv2
1104 .. code-block:: yaml
1106 policies:
1107 - name: ec2-require-imdsv2
1108 resource: ec2
1109 filters:
1110 - MetadataOptions.HttpTokens: optional
1111 actions:
1112 - type: set-metadata-access
1113 tokens: required
1115 :example:
1117 Disable metadata server access
1119 .. code-block: yaml
1121 policies:
1122 - name: ec2-disable-imds
1123 resource: ec2
1124 filters:
1125 - MetadataOptions.HttpEndpoint: enabled
1126 actions:
1127 - type: set-metadata-access
1128 endpoint: disabled
1130 policies:
1131 - name: ec2-enable-metadata-tags
1132 resource: ec2
1133 filters:
1134 - MetadataOptions.InstanceMetadataTags: disabled
1135 actions:
1136 - type: set-metadata-access
1137 metadata-tags: enabled
1139 Reference: https://amzn.to/2XOuxpQ
1140 """
1142 AllowedValues = {
1143 'HttpEndpoint': ['enabled', 'disabled'],
1144 'HttpTokens': ['required', 'optional'],
1145 'InstanceMetadataTags': ['enabled', 'disabled'],
1146 'HttpPutResponseHopLimit': list(range(1, 65))
1147 }
1149 schema = type_schema(
1150 'set-metadata-access',
1151 anyOf=[{'required': ['endpoint']},
1152 {'required': ['tokens']},
1153 {'required': ['metadatatags']},
1154 {'required': ['hop-limit']}],
1155 **{'endpoint': {'enum': AllowedValues['HttpEndpoint']},
1156 'tokens': {'enum': AllowedValues['HttpTokens']},
1157 'metadata-tags': {'enum': AllowedValues['InstanceMetadataTags']},
1158 'hop-limit': {'type': 'integer', 'minimum': 1, 'maximum': 64}}
1159 )
1160 permissions = ('ec2:ModifyInstanceMetadataOptions',)
1162 def get_params(self):
1163 return filter_empty({
1164 'HttpEndpoint': self.data.get('endpoint'),
1165 'HttpTokens': self.data.get('tokens'),
1166 'InstanceMetadataTags': self.data.get('metadata-tags'),
1167 'HttpPutResponseHopLimit': self.data.get('hop-limit')})
1169 def process(self, resources):
1170 params = self.get_params()
1171 for k, v in params.items():
1172 allowed_values = list(self.AllowedValues[k])
1173 allowed_values.remove(v)
1174 resources = self.filter_resources(
1175 resources, 'MetadataOptions.%s' % k, allowed_values)
1177 if not resources:
1178 return
1180 client = utils.local_session(self.manager.session_factory).client('ec2')
1181 for r in resources:
1182 self.manager.retry(
1183 client.modify_instance_metadata_options,
1184 ignore_err_codes=('InvalidInstanceId.NotFound',),
1185 InstanceId=r['InstanceId'],
1186 **params)
1189@EC2.action_registry.register("post-finding")
1190class InstanceFinding(PostFinding):
1192 resource_type = 'AwsEc2Instance'
1194 def format_resource(self, r):
1195 ip_addresses = jmespath_search(
1196 "NetworkInterfaces[].PrivateIpAddresses[].PrivateIpAddress", r)
1198 # limit to max 10 ip addresses, per security hub service limits
1199 ip_addresses = ip_addresses and ip_addresses[:10] or ip_addresses
1200 details = {
1201 "Type": r["InstanceType"],
1202 "ImageId": r["ImageId"],
1203 "IpV4Addresses": ip_addresses,
1204 "KeyName": r.get("KeyName"),
1205 "LaunchedAt": r["LaunchTime"].isoformat()
1206 }
1208 if "VpcId" in r:
1209 details["VpcId"] = r["VpcId"]
1210 if "SubnetId" in r:
1211 details["SubnetId"] = r["SubnetId"]
1212 # config will use an empty key
1213 if "IamInstanceProfile" in r and r['IamInstanceProfile']:
1214 details["IamInstanceProfileArn"] = r["IamInstanceProfile"]["Arn"]
1216 instance = {
1217 "Type": self.resource_type,
1218 "Id": "arn:{}:ec2:{}:{}:instance/{}".format(
1219 utils.REGION_PARTITION_MAP.get(self.manager.config.region, 'aws'),
1220 self.manager.config.region,
1221 self.manager.config.account_id,
1222 r["InstanceId"]),
1223 "Region": self.manager.config.region,
1224 "Tags": {t["Key"]: t["Value"] for t in r.get("Tags", [])},
1225 "Details": {self.resource_type: filter_empty(details)},
1226 }
1228 instance = filter_empty(instance)
1229 return instance
1232@actions.register('start')
1233class Start(BaseAction):
1234 """Starts a previously stopped EC2 instance.
1236 :Example:
1238 .. code-block:: yaml
1240 policies:
1241 - name: ec2-start-stopped-instances
1242 resource: ec2
1243 query:
1244 - instance-state-name: stopped
1245 actions:
1246 - start
1248 http://docs.aws.amazon.com/cli/latest/reference/ec2/start-instances.html
1249 """
1251 valid_origin_states = ('stopped',)
1252 schema = type_schema('start')
1253 permissions = ('ec2:StartInstances',)
1254 batch_size = 10
1255 exception = None
1257 def _filter_ec2_with_volumes(self, instances):
1258 return [i for i in instances if len(i['BlockDeviceMappings']) > 0]
1260 def process(self, instances):
1261 instances = self._filter_ec2_with_volumes(
1262 self.filter_resources(instances, 'State.Name', self.valid_origin_states))
1263 if not len(instances):
1264 return
1266 client = utils.local_session(self.manager.session_factory).client('ec2')
1267 failures = {}
1269 # Play nice around aws having insufficient capacity...
1270 for itype, t_instances in utils.group_by(
1271 instances, 'InstanceType').items():
1272 for izone, z_instances in utils.group_by(
1273 t_instances, 'Placement.AvailabilityZone').items():
1274 for batch in utils.chunks(z_instances, self.batch_size):
1275 fails = self.process_instance_set(client, batch, itype, izone)
1276 if fails:
1277 failures["%s %s" % (itype, izone)] = [i['InstanceId'] for i in batch]
1279 if failures:
1280 fail_count = sum(map(len, failures.values()))
1281 msg = "Could not start %d of %d instances %s" % (
1282 fail_count, len(instances), utils.dumps(failures))
1283 self.log.warning(msg)
1284 raise RuntimeError(msg)
1286 def process_instance_set(self, client, instances, itype, izone):
1287 # Setup retry with insufficient capacity as well
1288 retryable = ('InsufficientInstanceCapacity', 'RequestLimitExceeded',
1289 'Client.RequestLimitExceeded', 'Server.InsufficientInstanceCapacity'),
1290 retry = utils.get_retry(retryable, max_attempts=5)
1291 instance_ids = [i['InstanceId'] for i in instances]
1292 while instance_ids:
1293 try:
1294 retry(client.start_instances, InstanceIds=instance_ids)
1295 break
1296 except ClientError as e:
1297 if e.response['Error']['Code'] in retryable:
1298 # we maxed out on our retries
1299 return True
1300 elif e.response['Error']['Code'] == 'IncorrectInstanceState':
1301 instance_ids.remove(extract_instance_id(e))
1302 else:
1303 raise
1306def extract_instance_id(state_error):
1307 "Extract an instance id from an error"
1308 instance_id = None
1309 match = RE_ERROR_INSTANCE_ID.search(str(state_error))
1310 if match:
1311 instance_id = match.groupdict().get('instance_id')
1312 if match is None or instance_id is None:
1313 raise ValueError("Could not extract instance id from error: %s" % state_error)
1314 return instance_id
1317@actions.register('resize')
1318class Resize(BaseAction):
1319 """Change an instance's size.
1321 An instance can only be resized when its stopped, this action
1322 can optionally restart an instance if needed to effect the instance
1323 type change. Instances are always left in the run state they were
1324 found in.
1326 There are a few caveats to be aware of, instance resizing
1327 needs to maintain compatibility for architecture, virtualization type
1328 hvm/pv, and ebs optimization at minimum.
1330 http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-resize.html
1331 """
1333 schema = type_schema(
1334 'resize',
1335 **{'restart': {'type': 'boolean'},
1336 'type-map': {'type': 'object'},
1337 'default': {'type': 'string'}})
1339 valid_origin_states = ('running', 'stopped')
1341 def get_permissions(self):
1342 perms = ('ec2:DescribeInstances', 'ec2:ModifyInstanceAttribute')
1343 if self.data.get('restart', False):
1344 perms += ('ec2:StopInstances', 'ec2:StartInstances')
1345 return perms
1347 def process(self, resources):
1348 stopped_instances = self.filter_resources(resources, 'State.Name', ('stopped',))
1349 running_instances = self.filter_resources(resources, 'State.Name', ('running',))
1351 if self.data.get('restart') and running_instances:
1352 Stop({'terminate-ephemeral': False},
1353 self.manager).process(running_instances)
1354 client = utils.local_session(
1355 self.manager.session_factory).client('ec2')
1356 waiter = client.get_waiter('instance_stopped')
1357 try:
1358 waiter.wait(
1359 InstanceIds=[r['InstanceId'] for r in running_instances])
1360 except ClientError as e:
1361 self.log.exception(
1362 "Exception stopping instances for resize:\n %s" % e)
1364 for instance_set in utils.chunks(itertools.chain(
1365 stopped_instances, running_instances), 20):
1366 self.process_resource_set(instance_set)
1368 if self.data.get('restart') and running_instances:
1369 client.start_instances(
1370 InstanceIds=[i['InstanceId'] for i in running_instances])
1371 return list(itertools.chain(stopped_instances, running_instances))
1373 def process_resource_set(self, instance_set):
1374 type_map = self.data.get('type-map')
1375 default_type = self.data.get('default')
1377 client = utils.local_session(
1378 self.manager.session_factory).client('ec2')
1380 for i in instance_set:
1381 self.log.debug(
1382 "resizing %s %s" % (i['InstanceId'], i['InstanceType']))
1383 new_type = type_map.get(i['InstanceType'], default_type)
1384 if new_type == i['InstanceType']:
1385 continue
1386 try:
1387 client.modify_instance_attribute(
1388 InstanceId=i['InstanceId'],
1389 InstanceType={'Value': new_type})
1390 except ClientError as e:
1391 self.log.exception(
1392 "Exception resizing instance:%s new:%s old:%s \n %s" % (
1393 i['InstanceId'], new_type, i['InstanceType'], e))
1396@actions.register('stop')
1397class Stop(BaseAction):
1398 """Stops or hibernates a running EC2 instances
1400 :Example:
1402 .. code-block:: yaml
1404 policies:
1405 - name: ec2-stop-running-instances
1406 resource: ec2
1407 query:
1408 - instance-state-name: running
1409 actions:
1410 - stop
1412 - name: ec2-hibernate-instances
1413 resources: ec2
1414 query:
1415 - instance-state-name: running
1416 actions:
1417 - type: stop
1418 hibernate: true
1421 Note when using hiberate, instances not configured for hiberation
1422 will just be stopped.
1423 """
1424 valid_origin_states = ('running',)
1426 schema = type_schema(
1427 'stop',
1428 **{
1429 "terminate-ephemeral": {"type": "boolean"},
1430 "hibernate": {"type": "boolean"},
1431 "force": {"type": "boolean"},
1432 },
1433 )
1435 has_hibernate = jmespath_compile('[].HibernationOptions.Configured')
1437 def get_permissions(self):
1438 perms = ('ec2:StopInstances',)
1439 if self.data.get('terminate-ephemeral', False):
1440 perms += ('ec2:TerminateInstances',)
1441 if self.data.get("force"):
1442 perms += ("ec2:ModifyInstanceAttribute",)
1443 return perms
1445 def split_on_storage(self, instances):
1446 ephemeral = []
1447 persistent = []
1448 for i in instances:
1449 if EphemeralInstanceFilter.is_ephemeral(i):
1450 ephemeral.append(i)
1451 else:
1452 persistent.append(i)
1453 return ephemeral, persistent
1455 def split_on_hibernate(self, instances):
1456 enabled, disabled = [], []
1457 for status, i in zip(self.has_hibernate.search(instances), instances):
1458 if status is True:
1459 enabled.append(i)
1460 else:
1461 disabled.append(i)
1462 return enabled, disabled
1464 def process(self, instances):
1465 instances = self.filter_resources(instances, 'State.Name', self.valid_origin_states)
1466 if not len(instances):
1467 return
1468 client = utils.local_session(
1469 self.manager.session_factory).client('ec2')
1470 # Ephemeral instance can't be stopped.
1471 ephemeral, persistent = self.split_on_storage(instances)
1472 if self.data.get('terminate-ephemeral', False) and ephemeral:
1473 self._run_instances_op(client, 'terminate', ephemeral)
1474 if persistent:
1475 if self.data.get('hibernate', False):
1476 enabled, persistent = self.split_on_hibernate(persistent)
1477 if enabled:
1478 self._run_instances_op(client, 'stop', enabled, Hibernate=True)
1479 self._run_instances_op(client, 'stop', persistent)
1480 return instances
1482 def disable_protection(self, client, op, instances):
1483 def modify_instance(i, attribute):
1484 try:
1485 self.manager.retry(
1486 client.modify_instance_attribute,
1487 InstanceId=i['InstanceId'],
1488 Attribute=attribute,
1489 Value='false',
1490 )
1491 except ClientError as e:
1492 if e.response['Error']['Code'] == 'IncorrectInstanceState':
1493 return
1494 raise
1496 def process_instance(i, op):
1497 modify_instance(i, 'disableApiStop')
1498 if op == 'terminate':
1499 modify_instance(i, 'disableApiTermination')
1501 with self.executor_factory(max_workers=2) as w:
1502 list(w.map(process_instance, instances, [op] * len(instances)))
1504 def _run_instances_op(self, client, op, instances, **kwargs):
1505 client_op = client.stop_instances
1506 if op == 'terminate':
1507 client_op = client.terminate_instances
1509 instance_ids = [i['InstanceId'] for i in instances]
1511 while instances:
1512 try:
1513 return self.manager.retry(client_op, InstanceIds=instance_ids, **kwargs)
1514 except ClientError as e:
1515 if e.response['Error']['Code'] == 'IncorrectInstanceState':
1516 instance_ids.remove(extract_instance_id(e))
1517 if (
1518 e.response['Error']['Code'] == 'OperationNotPermitted' and
1519 self.data.get('force')
1520 ):
1521 self.log.info("Disabling stop and termination protection on instances")
1522 self.disable_protection(
1523 client,
1524 op,
1525 [i for i in instances if i.get('InstanceLifecycle') != 'spot'],
1526 )
1527 continue
1528 raise
1531@actions.register('reboot')
1532class Reboot(BaseAction):
1533 """Reboots a previously running EC2 instance.
1535 :Example:
1537 .. code-block:: yaml
1539 policies:
1540 - name: ec2-reboot-instances
1541 resource: ec2
1542 query:
1543 - instance-state-name: running
1544 actions:
1545 - reboot
1547 http://docs.aws.amazon.com/cli/latest/reference/ec2/reboot-instances.html
1548 """
1550 valid_origin_states = ('running',)
1551 schema = type_schema('reboot')
1552 permissions = ('ec2:RebootInstances',)
1553 batch_size = 10
1554 exception = None
1556 def _filter_ec2_with_volumes(self, instances):
1557 return [i for i in instances if len(i['BlockDeviceMappings']) > 0]
1559 def process(self, instances):
1560 instances = self._filter_ec2_with_volumes(
1561 self.filter_resources(instances, 'State.Name', self.valid_origin_states))
1562 if not len(instances):
1563 return
1565 client = utils.local_session(self.manager.session_factory).client('ec2')
1566 failures = {}
1568 for batch in utils.chunks(instances, self.batch_size):
1569 fails = self.process_instance_set(client, batch)
1570 if fails:
1571 failures = [i['InstanceId'] for i in batch]
1573 if failures:
1574 fail_count = sum(map(len, failures.values()))
1575 msg = "Could not reboot %d of %d instances %s" % (
1576 fail_count, len(instances),
1577 utils.dumps(failures))
1578 self.log.warning(msg)
1579 raise RuntimeError(msg)
1581 def process_instance_set(self, client, instances):
1582 # Setup retry with insufficient capacity as well
1583 retryable = ('InsufficientInstanceCapacity', 'RequestLimitExceeded',
1584 'Client.RequestLimitExceeded'),
1585 retry = utils.get_retry(retryable, max_attempts=5)
1586 instance_ids = [i['InstanceId'] for i in instances]
1587 try:
1588 retry(client.reboot_instances, InstanceIds=instance_ids)
1589 except ClientError as e:
1590 if e.response['Error']['Code'] in retryable:
1591 return True
1592 raise
1595@actions.register('terminate')
1596class Terminate(BaseAction):
1597 """ Terminate a set of instances.
1599 While ec2 offers a bulk delete api, any given instance can be configured
1600 with api deletion termination protection, so we can't use the bulk call
1601 reliabily, we need to process the instances individually. Additionally
1602 If we're configured with 'force' then we'll turn off instance termination
1603 and stop protection.
1605 :Example:
1607 .. code-block:: yaml
1609 policies:
1610 - name: ec2-process-termination
1611 resource: ec2
1612 filters:
1613 - type: marked-for-op
1614 op: terminate
1615 actions:
1616 - terminate
1617 """
1619 valid_origin_states = ('running', 'stopped', 'pending', 'stopping')
1621 schema = type_schema('terminate', force={'type': 'boolean'})
1623 def get_permissions(self):
1624 permissions = ("ec2:TerminateInstances",)
1625 if self.data.get('force'):
1626 permissions += ('ec2:ModifyInstanceAttribute',)
1627 return permissions
1629 def process_terminate(self, instances):
1630 client = utils.local_session(
1631 self.manager.session_factory).client('ec2')
1632 try:
1633 self.manager.retry(
1634 client.terminate_instances,
1635 InstanceIds=[i['InstanceId'] for i in instances])
1636 return
1637 except ClientError as e:
1638 if e.response['Error']['Code'] != 'OperationNotPermitted':
1639 raise
1640 if not self.data.get('force'):
1641 raise
1643 self.log.info("Disabling stop and termination protection on instances")
1644 self.disable_deletion_protection(
1645 client,
1646 [i for i in instances if i.get('InstanceLifecycle') != 'spot'])
1647 self.manager.retry(
1648 client.terminate_instances,
1649 InstanceIds=[i['InstanceId'] for i in instances])
1651 def process(self, instances):
1652 instances = self.filter_resources(instances, 'State.Name', self.valid_origin_states)
1653 if not len(instances):
1654 return
1655 # limit batch sizes to avoid api limits
1656 for batch in utils.chunks(instances, 100):
1657 self.process_terminate(batch)
1659 def disable_deletion_protection(self, client, instances):
1661 def modify_instance(i, attribute):
1662 try:
1663 self.manager.retry(
1664 client.modify_instance_attribute,
1665 InstanceId=i['InstanceId'],
1666 Attribute=attribute,
1667 Value='false')
1668 except ClientError as e:
1669 if e.response['Error']['Code'] == 'IncorrectInstanceState':
1670 return
1671 raise
1673 def process_instance(i):
1674 modify_instance(i, 'disableApiTermination')
1675 modify_instance(i, 'disableApiStop')
1677 with self.executor_factory(max_workers=2) as w:
1678 list(w.map(process_instance, instances))
1681@actions.register('snapshot')
1682class Snapshot(BaseAction):
1683 """Snapshot the volumes attached to an EC2 instance.
1685 Tags may be optionally added to the snapshot during creation.
1687 - `copy-volume-tags` copies all the tags from the specified
1688 volume to the corresponding snapshot.
1689 - `copy-tags` copies the listed tags from each volume
1690 to the snapshot. This is mutually exclusive with
1691 `copy-volume-tags`.
1692 - `tags` allows new tags to be added to each snapshot when using
1693 'copy-tags`. If no tags are specified, then the tag
1694 `custodian_snapshot` is added.
1696 The default behavior is `copy-volume-tags: true`.
1698 :Example:
1700 .. code-block:: yaml
1702 policies:
1703 - name: ec2-snapshots
1704 resource: ec2
1705 actions:
1706 - type: snapshot
1707 copy-tags:
1708 - Name
1709 tags:
1710 custodian_snapshot: True
1711 """
1713 schema = type_schema(
1714 'snapshot',
1715 **{'copy-tags': {'type': 'array', 'items': {'type': 'string'}},
1716 'copy-volume-tags': {'type': 'boolean'},
1717 'tags': {'type': 'object'},
1718 'exclude-boot': {'type': 'boolean', 'default': False}})
1719 permissions = ('ec2:CreateSnapshot', 'ec2:CreateTags',)
1721 def validate(self):
1722 if self.data.get('copy-tags') and 'copy-volume-tags' in self.data:
1723 raise PolicyValidationError(
1724 "Can specify copy-tags or copy-volume-tags, not both")
1726 def process(self, resources):
1727 client = utils.local_session(self.manager.session_factory).client('ec2')
1728 err = None
1729 with self.executor_factory(max_workers=2) as w:
1730 futures = {}
1731 for resource in resources:
1732 futures[w.submit(
1733 self.process_volume_set, client, resource)] = resource
1734 for f in as_completed(futures):
1735 if f.exception():
1736 err = f.exception()
1737 resource = futures[f]
1738 self.log.error(
1739 "Exception creating snapshot set instance:%s \n %s" % (
1740 resource['InstanceId'], err))
1741 if err:
1742 raise err
1744 def process_volume_set(self, client, resource):
1745 params = dict(
1746 InstanceSpecification={
1747 'ExcludeBootVolume': self.data.get('exclude-boot', False),
1748 'InstanceId': resource['InstanceId']})
1749 if 'copy-tags' in self.data:
1750 params['TagSpecifications'] = [{
1751 'ResourceType': 'snapshot',
1752 'Tags': self.get_snapshot_tags(resource)}]
1753 elif self.data.get('copy-volume-tags', True):
1754 params['CopyTagsFromSource'] = 'volume'
1756 try:
1757 result = self.manager.retry(client.create_snapshots, **params)
1758 resource['c7n:snapshots'] = [
1759 s['SnapshotId'] for s in result['Snapshots']]
1760 except ClientError as e:
1761 err_code = e.response['Error']['Code']
1762 if err_code not in (
1763 'InvalidInstanceId.NotFound',
1764 'ConcurrentSnapshotLimitExceeded',
1765 'IncorrectState'):
1766 raise
1767 self.log.warning(
1768 "action:snapshot instance:%s error:%s",
1769 resource['InstanceId'], err_code)
1771 def get_snapshot_tags(self, resource):
1772 user_tags = self.data.get('tags', {}) or {'custodian_snapshot': ''}
1773 copy_tags = self.data.get('copy-tags', [])
1774 return coalesce_copy_user_tags(resource, copy_tags, user_tags)
1777@actions.register('modify-security-groups')
1778class EC2ModifyVpcSecurityGroups(ModifyVpcSecurityGroupsAction):
1779 """Modify security groups on an instance."""
1781 permissions = ("ec2:ModifyNetworkInterfaceAttribute",)
1782 sg_expr = jmespath_compile("Groups[].GroupId")
1784 def process(self, instances):
1785 if not len(instances):
1786 return
1787 client = utils.local_session(
1788 self.manager.session_factory).client('ec2')
1790 # handle multiple ENIs
1791 interfaces = []
1792 for i in instances:
1793 for eni in i['NetworkInterfaces']:
1794 if i.get('c7n:matched-security-groups'):
1795 eni['c7n:matched-security-groups'] = i[
1796 'c7n:matched-security-groups']
1797 if i.get('c7n:NetworkLocation'):
1798 eni['c7n:NetworkLocation'] = i[
1799 'c7n:NetworkLocation']
1800 interfaces.append(eni)
1802 groups = super(EC2ModifyVpcSecurityGroups, self).get_groups(interfaces)
1804 for idx, i in enumerate(interfaces):
1805 client.modify_network_interface_attribute(
1806 NetworkInterfaceId=i['NetworkInterfaceId'],
1807 Groups=groups[idx])
1810@actions.register('autorecover-alarm')
1811class AutorecoverAlarm(BaseAction):
1812 """Adds a cloudwatch metric alarm to recover an EC2 instance.
1814 This action takes effect on instances that are NOT part
1815 of an ASG.
1817 :Example:
1819 .. code-block:: yaml
1821 policies:
1822 - name: ec2-autorecover-alarm
1823 resource: ec2
1824 filters:
1825 - singleton
1826 actions:
1827 - autorecover-alarm
1829 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-recover.html
1830 """
1832 schema = type_schema('autorecover-alarm')
1833 permissions = ('cloudwatch:PutMetricAlarm',)
1834 valid_origin_states = ('running', 'stopped', 'pending', 'stopping')
1835 filter_asg_membership = ValueFilter({
1836 'key': 'tag:aws:autoscaling:groupName',
1837 'value': 'empty'}).validate()
1839 def process(self, instances):
1840 instances = self.filter_asg_membership.process(
1841 self.filter_resources(instances, 'State.Name', self.valid_origin_states))
1842 if not len(instances):
1843 return
1844 client = utils.local_session(
1845 self.manager.session_factory).client('cloudwatch')
1846 for i in instances:
1847 client.put_metric_alarm(
1848 AlarmName='recover-{}'.format(i['InstanceId']),
1849 AlarmDescription='Auto Recover {}'.format(i['InstanceId']),
1850 ActionsEnabled=True,
1851 AlarmActions=[
1852 'arn:{}:automate:{}:ec2:recover'.format(
1853 utils.REGION_PARTITION_MAP.get(
1854 self.manager.config.region, 'aws'),
1855 i['Placement']['AvailabilityZone'][:-1])
1856 ],
1857 MetricName='StatusCheckFailed_System',
1858 Namespace='AWS/EC2',
1859 Statistic='Minimum',
1860 Dimensions=[
1861 {
1862 'Name': 'InstanceId',
1863 'Value': i['InstanceId']
1864 }
1865 ],
1866 Period=60,
1867 EvaluationPeriods=2,
1868 Threshold=0,
1869 ComparisonOperator='GreaterThanThreshold'
1870 )
1873@actions.register('set-instance-profile')
1874class SetInstanceProfile(BaseAction):
1875 """Sets (add, modify, remove) the instance profile for a running EC2 instance.
1877 :Example:
1879 .. code-block:: yaml
1881 policies:
1882 - name: set-default-instance-profile
1883 resource: ec2
1884 filters:
1885 - IamInstanceProfile: absent
1886 actions:
1887 - type: set-instance-profile
1888 name: default
1890 https://docs.aws.amazon.com/cli/latest/reference/ec2/associate-iam-instance-profile.html
1891 https://docs.aws.amazon.com/cli/latest/reference/ec2/disassociate-iam-instance-profile.html
1892 """
1894 schema = type_schema(
1895 'set-instance-profile',
1896 **{'name': {'type': 'string'}})
1898 permissions = (
1899 'ec2:AssociateIamInstanceProfile',
1900 'ec2:DisassociateIamInstanceProfile',
1901 'iam:PassRole')
1903 valid_origin_states = ('running', 'pending', 'stopped', 'stopping')
1905 def process(self, instances):
1906 instances = self.filter_resources(instances, 'State.Name', self.valid_origin_states)
1907 if not len(instances):
1908 return
1909 client = utils.local_session(self.manager.session_factory).client('ec2')
1910 profile_name = self.data.get('name')
1911 profile_instances = [i for i in instances if i.get('IamInstanceProfile')]
1913 if profile_instances:
1914 associations = {
1915 a['InstanceId']: (a['AssociationId'], a['IamInstanceProfile']['Arn'])
1916 for a in client.describe_iam_instance_profile_associations(
1917 Filters=[
1918 {'Name': 'instance-id',
1919 'Values': [i['InstanceId'] for i in profile_instances]},
1920 {'Name': 'state', 'Values': ['associating', 'associated']}]
1921 ).get('IamInstanceProfileAssociations', ())}
1922 else:
1923 associations = {}
1925 for i in instances:
1926 if profile_name and i['InstanceId'] not in associations:
1927 client.associate_iam_instance_profile(
1928 IamInstanceProfile={'Name': profile_name},
1929 InstanceId=i['InstanceId'])
1930 continue
1931 # Removing profile and no profile on instance.
1932 elif profile_name is None and i['InstanceId'] not in associations:
1933 continue
1935 p_assoc_id, p_arn = associations[i['InstanceId']]
1937 # Already associated to target profile, skip
1938 if profile_name and p_arn.endswith('/%s' % profile_name):
1939 continue
1941 if profile_name is None:
1942 client.disassociate_iam_instance_profile(
1943 AssociationId=p_assoc_id)
1944 else:
1945 client.replace_iam_instance_profile_association(
1946 IamInstanceProfile={'Name': profile_name},
1947 AssociationId=p_assoc_id)
1949 return instances
1952@actions.register('propagate-spot-tags')
1953class PropagateSpotTags(BaseAction):
1954 """Propagate Tags that are set at Spot Request level to EC2 instances.
1956 :Example:
1958 .. code-block:: yaml
1960 policies:
1961 - name: ec2-spot-instances
1962 resource: ec2
1963 filters:
1964 - State.Name: pending
1965 - instanceLifecycle: spot
1966 actions:
1967 - type: propagate-spot-tags
1968 only_tags:
1969 - Name
1970 - BillingTag
1971 """
1973 schema = type_schema(
1974 'propagate-spot-tags',
1975 **{'only_tags': {'type': 'array', 'items': {'type': 'string'}}})
1977 permissions = (
1978 'ec2:DescribeInstances',
1979 'ec2:DescribeSpotInstanceRequests',
1980 'ec2:DescribeTags',
1981 'ec2:CreateTags')
1983 MAX_TAG_COUNT = 50
1985 def process(self, instances):
1986 instances = [
1987 i for i in instances if i['InstanceLifecycle'] == 'spot']
1988 if not len(instances):
1989 self.log.warning(
1990 "action:%s no spot instances found, implicit filter by action" % (
1991 self.__class__.__name__.lower()))
1992 return
1994 client = utils.local_session(
1995 self.manager.session_factory).client('ec2')
1997 request_instance_map = {}
1998 for i in instances:
1999 request_instance_map.setdefault(
2000 i['SpotInstanceRequestId'], []).append(i)
2002 # ... and describe the corresponding spot requests ...
2003 requests = client.describe_spot_instance_requests(
2004 Filters=[{
2005 'Name': 'spot-instance-request-id',
2006 'Values': list(request_instance_map.keys())}]).get(
2007 'SpotInstanceRequests', [])
2009 updated = []
2010 for r in requests:
2011 if not r.get('Tags'):
2012 continue
2013 updated.extend(
2014 self.process_request_instances(
2015 client, r, request_instance_map[r['SpotInstanceRequestId']]))
2016 return updated
2018 def process_request_instances(self, client, request, instances):
2019 # Now we find the tags we can copy : either all, either those
2020 # indicated with 'only_tags' parameter.
2021 copy_keys = self.data.get('only_tags', [])
2022 request_tags = {t['Key']: t['Value'] for t in request['Tags']
2023 if not t['Key'].startswith('aws:')}
2024 if copy_keys:
2025 for k in set(copy_keys).difference(request_tags):
2026 del request_tags[k]
2028 update_instances = []
2029 for i in instances:
2030 instance_tags = {t['Key']: t['Value'] for t in i.get('Tags', [])}
2031 # We may overwrite tags, but if the operation changes no tag,
2032 # we will not proceed.
2033 for k, v in request_tags.items():
2034 if k not in instance_tags or instance_tags[k] != v:
2035 update_instances.append(i['InstanceId'])
2037 if len(set(instance_tags) | set(request_tags)) > self.MAX_TAG_COUNT:
2038 self.log.warning(
2039 "action:%s instance:%s too many tags to copy (> 50)" % (
2040 self.__class__.__name__.lower(),
2041 i['InstanceId']))
2042 continue
2044 for iset in utils.chunks(update_instances, 20):
2045 client.create_tags(
2046 DryRun=self.manager.config.dryrun,
2047 Resources=iset,
2048 Tags=[{'Key': k, 'Value': v} for k, v in request_tags.items()])
2050 self.log.debug(
2051 "action:%s tags updated on instances:%r" % (
2052 self.__class__.__name__.lower(),
2053 update_instances))
2055 return update_instances
2058# Valid EC2 Query Filters
2059# http://docs.aws.amazon.com/AWSEC2/latest/CommandLineReference/ApiReference-cmd-DescribeInstances.html
2060EC2_VALID_FILTERS = {
2061 'architecture': ('i386', 'x86_64'),
2062 'availability-zone': str,
2063 'iam-instance-profile.arn': str,
2064 'image-id': str,
2065 'instance-id': str,
2066 'instance-lifecycle': ('spot',),
2067 'instance-state-name': (
2068 'pending',
2069 'terminated',
2070 'running',
2071 'shutting-down',
2072 'stopping',
2073 'stopped'),
2074 'instance.group-id': str,
2075 'instance.group-name': str,
2076 'tag-key': str,
2077 'tag-value': str,
2078 'tag:': str,
2079 'tenancy': ('dedicated', 'default', 'host'),
2080 'vpc-id': str}
2083class QueryFilter:
2085 @classmethod
2086 def parse(cls, data):
2087 results = []
2088 for d in data:
2089 if not isinstance(d, dict):
2090 raise ValueError(
2091 "EC2 Query Filter Invalid structure %s" % d)
2092 results.append(cls(d).validate())
2093 return results
2095 def __init__(self, data):
2096 self.data = data
2097 self.key = None
2098 self.value = None
2100 def validate(self):
2101 if not len(list(self.data.keys())) == 1:
2102 raise PolicyValidationError(
2103 "EC2 Query Filter Invalid %s" % self.data)
2104 self.key = list(self.data.keys())[0]
2105 self.value = list(self.data.values())[0]
2107 if self.key not in EC2_VALID_FILTERS and not self.key.startswith(
2108 'tag:'):
2109 raise PolicyValidationError(
2110 "EC2 Query Filter invalid filter name %s" % (self.data))
2112 if self.value is None:
2113 raise PolicyValidationError(
2114 "EC2 Query Filters must have a value, use tag-key"
2115 " w/ tag name as value for tag present checks"
2116 " %s" % self.data)
2117 return self
2119 def query(self):
2120 value = self.value
2121 if isinstance(self.value, str):
2122 value = [self.value]
2124 return {'Name': self.key, 'Values': value}
2127@filters.register('instance-attribute')
2128class InstanceAttribute(ValueFilter):
2129 """EC2 Instance Value Filter on a given instance attribute.
2131 Filters EC2 Instances with the given instance attribute
2133 :Example:
2135 .. code-block:: yaml
2137 policies:
2138 - name: ec2-unoptimized-ebs
2139 resource: ec2
2140 filters:
2141 - type: instance-attribute
2142 attribute: ebsOptimized
2143 key: "Value"
2144 value: false
2145 """
2147 valid_attrs = (
2148 'instanceType',
2149 'kernel',
2150 'ramdisk',
2151 'userData',
2152 'disableApiTermination',
2153 'instanceInitiatedShutdownBehavior',
2154 'rootDeviceName',
2155 'blockDeviceMapping',
2156 'productCodes',
2157 'sourceDestCheck',
2158 'groupSet',
2159 'ebsOptimized',
2160 'sriovNetSupport',
2161 'enaSupport')
2163 schema = type_schema(
2164 'instance-attribute',
2165 rinherit=ValueFilter.schema,
2166 attribute={'enum': valid_attrs},
2167 required=('attribute',))
2168 schema_alias = False
2170 def get_permissions(self):
2171 return ('ec2:DescribeInstanceAttribute',)
2173 def process(self, resources, event=None):
2174 attribute = self.data['attribute']
2175 self.get_instance_attribute(resources, attribute)
2176 return [resource for resource in resources
2177 if self.match(resource['c7n:attribute-%s' % attribute])]
2179 def get_instance_attribute(self, resources, attribute):
2180 client = utils.local_session(
2181 self.manager.session_factory).client('ec2')
2183 for resource in resources:
2184 instance_id = resource['InstanceId']
2185 fetched_attribute = self.manager.retry(
2186 client.describe_instance_attribute,
2187 Attribute=attribute,
2188 InstanceId=instance_id)
2189 keys = list(fetched_attribute.keys())
2190 keys.remove('ResponseMetadata')
2191 keys.remove('InstanceId')
2192 resource['c7n:attribute-%s' % attribute] = fetched_attribute[
2193 keys[0]]
2196@resources.register('launch-template-version')
2197class LaunchTemplate(query.QueryResourceManager):
2199 class resource_type(query.TypeInfo):
2200 id = 'LaunchTemplateId'
2201 id_prefix = 'lt-'
2202 name = 'LaunchTemplateName'
2203 service = 'ec2'
2204 date = 'CreateTime'
2205 enum_spec = (
2206 'describe_launch_templates', 'LaunchTemplates', None)
2207 filter_name = 'LaunchTemplateIds'
2208 filter_type = 'list'
2209 arn_type = "launch-template"
2210 cfn_type = "AWS::EC2::LaunchTemplate"
2212 def augment(self, resources):
2213 client = utils.local_session(
2214 self.session_factory).client('ec2')
2215 template_versions = []
2216 for r in resources:
2217 template_versions.extend(
2218 client.describe_launch_template_versions(
2219 LaunchTemplateId=r['LaunchTemplateId']).get(
2220 'LaunchTemplateVersions', ()))
2221 return template_versions
2223 def get_arns(self, resources):
2224 arns = []
2225 for r in resources:
2226 arns.append(self.generate_arn(f"{r['LaunchTemplateId']}/{r['VersionNumber']}"))
2227 return arns
2229 def get_resources(self, rids, cache=True):
2230 # Launch template versions have a compound primary key
2231 #
2232 # Support one of four forms of resource ids:
2233 #
2234 # - array of launch template ids
2235 # - array of tuples (launch template id, version id)
2236 # - array of dicts (with LaunchTemplateId and VersionNumber)
2237 # - array of dicts (with LaunchTemplateId and LatestVersionNumber)
2238 #
2239 # If an alias version is given $Latest, $Default, the alias will be
2240 # preserved as an annotation on the returned object 'c7n:VersionAlias'
2241 if not rids:
2242 return []
2244 t_versions = {}
2245 if isinstance(rids[0], tuple):
2246 for tid, tversion in rids:
2247 t_versions.setdefault(tid, []).append(tversion)
2248 elif isinstance(rids[0], dict):
2249 for tinfo in rids:
2250 t_versions.setdefault(
2251 tinfo['LaunchTemplateId'], []).append(
2252 tinfo.get('VersionNumber', tinfo.get('LatestVersionNumber')))
2253 elif isinstance(rids[0], str):
2254 for tid in rids:
2255 t_versions[tid] = []
2257 client = utils.local_session(self.session_factory).client('ec2')
2259 results = []
2260 # We may end up fetching duplicates on $Latest and $Version
2261 for tid, tversions in t_versions.items():
2262 try:
2263 ltv = client.describe_launch_template_versions(
2264 LaunchTemplateId=tid, Versions=tversions).get(
2265 'LaunchTemplateVersions')
2266 except ClientError as e:
2267 if e.response['Error']['Code'] == "InvalidLaunchTemplateId.NotFound":
2268 continue
2269 if e.response['Error']['Code'] == "InvalidLaunchTemplateId.VersionNotFound":
2270 continue
2271 raise
2272 if not tversions:
2273 tversions = [str(t['VersionNumber']) for t in ltv]
2274 for tversion, t in zip(tversions, ltv):
2275 if not tversion.isdigit():
2276 t['c7n:VersionAlias'] = tversion
2277 results.append(t)
2278 return results
2280 def get_asg_templates(self, asgs):
2281 templates = {}
2282 for a in asgs:
2283 t = None
2284 if 'LaunchTemplate' in a:
2285 t = a['LaunchTemplate']
2286 elif 'MixedInstancesPolicy' in a:
2287 t = a['MixedInstancesPolicy'][
2288 'LaunchTemplate']['LaunchTemplateSpecification']
2289 if t is None:
2290 continue
2291 templates.setdefault(
2292 (t['LaunchTemplateId'],
2293 t.get('Version', '$Default')), []).append(a['AutoScalingGroupName'])
2294 return templates
2297@resources.register('ec2-reserved')
2298class ReservedInstance(query.QueryResourceManager):
2300 class resource_type(query.TypeInfo):
2301 service = 'ec2'
2302 name = id = 'ReservedInstancesId'
2303 id_prefix = ""
2304 date = 'Start'
2305 enum_spec = (
2306 'describe_reserved_instances', 'ReservedInstances', None)
2307 filter_name = 'ReservedInstancesIds'
2308 filter_type = 'list'
2309 arn_type = "reserved-instances"
2312@resources.register('ec2-host')
2313class DedicatedHost(query.QueryResourceManager):
2314 """Custodian resource for managing EC2 Dedicated Hosts.
2315 """
2317 class resource_type(query.TypeInfo):
2318 service = 'ec2'
2319 name = id = 'HostId'
2320 id_prefix = 'h-'
2321 enum_spec = ('describe_hosts', 'Hosts', None)
2322 arn_type = "dedicated-host"
2323 filter_name = 'HostIds'
2324 filter_type = 'list'
2325 date = 'AllocationTime'
2326 cfn_type = config_type = 'AWS::EC2::Host'
2327 permissions_enum = ('ec2:DescribeHosts',)
2330@resources.register('ec2-spot-fleet-request')
2331class SpotFleetRequest(query.QueryResourceManager):
2332 """Custodian resource for managing EC2 Spot Fleet Requests.
2333 """
2335 class resource_type(query.TypeInfo):
2336 service = 'ec2'
2337 name = id = 'SpotFleetRequestId'
2338 id_prefix = 'sfr-'
2339 enum_spec = ('describe_spot_fleet_requests', 'SpotFleetRequestConfigs', None)
2340 filter_name = 'SpotFleetRequestIds'
2341 filter_type = 'list'
2342 date = 'CreateTime'
2343 arn_type = 'spot-fleet-request'
2344 config_type = cfn_type = 'AWS::EC2::SpotFleet'
2345 permissions_enum = ('ec2:DescribeSpotFleetRequests',)
2348SpotFleetRequest.filter_registry.register('offhour', OffHour)
2349SpotFleetRequest.filter_registry.register('onhour', OnHour)
2352@SpotFleetRequest.action_registry.register('resize')
2353class AutoscalingSpotFleetRequest(AutoscalingBase):
2354 permissions = (
2355 'ec2:CreateTags',
2356 'ec2:ModifySpotFleetRequest',
2357 )
2359 service_namespace = 'ec2'
2360 scalable_dimension = 'ec2:spot-fleet-request:TargetCapacity'
2362 def get_resource_id(self, resource):
2363 return 'spot-fleet-request/%s' % resource['SpotFleetRequestId']
2365 def get_resource_tag(self, resource, key):
2366 if 'Tags' in resource:
2367 for tag in resource['Tags']:
2368 if tag['Key'] == key:
2369 return tag['Value']
2370 return None
2372 def get_resource_desired(self, resource):
2373 return int(resource['SpotFleetRequestConfig']['TargetCapacity'])
2375 def set_resource_desired(self, resource, desired):
2376 client = utils.local_session(self.manager.session_factory).client('ec2')
2377 client.modify_spot_fleet_request(
2378 SpotFleetRequestId=resource['SpotFleetRequestId'],
2379 TargetCapacity=desired,
2380 )
2383@EC2.filter_registry.register('has-specific-managed-policy')
2384class HasSpecificManagedPolicy(SpecificIamProfileManagedPolicy):
2385 """Filter an EC2 instance that has an IAM instance profile that contains an IAM role that has
2386 a specific managed IAM policy. If an EC2 instance does not have a profile or the profile
2387 does not contain an IAM role, then it will be treated as not having the policy.
2389 :example:
2391 .. code-block:: yaml
2393 policies:
2394 - name: ec2-instance-has-admin-policy
2395 resource: aws.ec2
2396 filters:
2397 - type: has-specific-managed-policy
2398 value: admin-policy
2400 :example:
2402 Check for EC2 instances with instance profile roles that have an
2403 attached policy matching a given list:
2405 .. code-block:: yaml
2407 policies:
2408 - name: ec2-instance-with-selected-policies
2409 resource: aws.ec2
2410 filters:
2411 - type: has-specific-managed-policy
2412 op: in
2413 value:
2414 - AmazonS3FullAccess
2415 - AWSOrganizationsFullAccess
2417 :example:
2419 Check for EC2 instances with instance profile roles that have
2420 attached policy names matching a pattern:
2422 .. code-block:: yaml
2424 policies:
2425 - name: ec2-instance-with-full-access-policies
2426 resource: aws.ec2
2427 filters:
2428 - type: has-specific-managed-policy
2429 op: glob
2430 value: "*FullAccess"
2432 Check for EC2 instances with instance profile roles that have
2433 attached policy ARNs matching a pattern:
2435 .. code-block:: yaml
2437 policies:
2438 - name: ec2-instance-with-aws-full-access-policies
2439 resource: aws.ec2
2440 filters:
2441 - type: has-specific-managed-policy
2442 key: PolicyArn
2443 op: regex
2444 value: "arn:aws:iam::aws:policy/.*FullAccess"
2445 """
2447 permissions = (
2448 'iam:GetInstanceProfile',
2449 'iam:ListInstanceProfiles',
2450 'iam:ListAttachedRolePolicies')
2452 def process(self, resources, event=None):
2453 client = utils.local_session(self.manager.session_factory).client('iam')
2454 iam_profiles = self.manager.get_resource_manager('iam-profile').resources()
2455 iam_profiles_mapping = {profile['Arn']: profile for profile in iam_profiles}
2457 results = []
2458 for r in resources:
2459 if r['State']['Name'] == 'terminated':
2460 continue
2461 instance_profile_arn = r.get('IamInstanceProfile', {}).get('Arn')
2462 if not instance_profile_arn:
2463 continue
2465 profile = iam_profiles_mapping.get(instance_profile_arn)
2466 if not profile:
2467 continue
2469 self.get_managed_policies(client, [profile])
2471 matched_keys = [k for k in profile[self.annotation_key] if self.match(k)]
2472 self.merge_annotation(profile, self.matched_annotation_key, matched_keys)
2473 if matched_keys:
2474 results.append(r)
2476 return results
2479@resources.register('ec2-capacity-reservation')
2480class CapacityReservation(query.QueryResourceManager):
2481 """Custodian resource for managing EC2 Capacity Reservation.
2482 """
2484 class resource_type(query.TypeInfo):
2485 name = id = 'CapacityReservationId'
2486 service = 'ec2'
2487 enum_spec = ('describe_capacity_reservations',
2488 'CapacityReservations', None)
2490 id_prefix = 'cr-'
2491 arn = "CapacityReservationArn"
2492 filter_name = 'CapacityReservationIds'
2493 filter_type = 'list'
2494 cfn_type = 'AWS::EC2::CapacityReservation'
2495 permissions_enum = ('ec2:DescribeCapacityReservations',)