Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/c7n/resources/ec2.py: 36%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 c7n.vendored.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
26from c7n.filters.costhub import CostHubRecommendation
27import c7n.filters.vpc as net_filters
29from c7n.manager import resources
30from c7n import query, utils
31from c7n.tags import coalesce_copy_user_tags
32from c7n.utils import type_schema, filter_empty, jmespath_search, jmespath_compile
34from c7n.resources.iam import CheckPermissions, SpecificIamProfileManagedPolicy
35from c7n.resources.securityhub import PostFinding
37RE_ERROR_INSTANCE_ID = re.compile("'(?P<instance_id>i-.*?)'")
39filters = FilterRegistry('ec2.filters')
40actions = ActionRegistry('ec2.actions')
43class DescribeEC2(query.DescribeSource):
45 def get_query_params(self, query_params):
46 queries = QueryFilter.parse(self.manager.data.get('query', []))
47 qf = []
48 for q in queries:
49 qd = q.query()
50 found = False
51 for f in qf:
52 if qd['Name'] == f['Name']:
53 f['Values'].extend(qd['Values'])
54 found = True
55 if not found:
56 qf.append(qd)
57 query_params = query_params or {}
58 query_params['Filters'] = qf
59 return query_params
61 def augment(self, resources):
62 """EC2 API and AWOL Tags
64 While ec2 api generally returns tags when doing describe_x on for
65 various resources, it may also silently fail to do so unless a tag
66 is used as a filter.
68 See footnote on for official documentation.
69 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#Using_Tags_CLI
71 Apriori we may be using custodian to ensure tags (including
72 name), so there isn't a good default to ensure that we will
73 always get tags from describe_x calls.
74 """
75 # First if we're in event based lambda go ahead and skip this,
76 # tags can't be trusted in ec2 instances immediately post creation.
77 if not resources or self.manager.data.get(
78 'mode', {}).get('type', '') in (
79 'cloudtrail', 'ec2-instance-state'):
80 return resources
82 # AWOL detector, so we don't make extraneous api calls.
83 resource_count = len(resources)
84 search_count = min(int(resource_count % 0.05) + 1, 5)
85 if search_count > resource_count:
86 search_count = resource_count
87 found = False
88 for r in random.sample(resources, search_count):
89 if 'Tags' in r:
90 found = True
91 break
93 if found:
94 return resources
96 # Okay go and do the tag lookup
97 client = utils.local_session(self.manager.session_factory).client('ec2')
98 tag_set = self.manager.retry(
99 client.describe_tags,
100 Filters=[{'Name': 'resource-type',
101 'Values': ['instance']}])['Tags']
102 resource_tags = {}
103 for t in tag_set:
104 t.pop('ResourceType')
105 rid = t.pop('ResourceId')
106 resource_tags.setdefault(rid, []).append(t)
108 m = self.manager.get_model()
109 for r in resources:
110 r['Tags'] = resource_tags.get(r[m.id], [])
111 return resources
114@resources.register('ec2')
115class EC2(query.QueryResourceManager):
117 class resource_type(query.TypeInfo):
118 service = 'ec2'
119 arn_type = 'instance'
120 enum_spec = ('describe_instances', 'Reservations[].Instances[]', None)
121 id = 'InstanceId'
122 filter_name = 'InstanceIds'
123 filter_type = 'list'
124 name = 'PublicDnsName'
125 date = 'LaunchTime'
126 dimension = 'InstanceId'
127 cfn_type = config_type = "AWS::EC2::Instance"
128 id_prefix = 'i-'
129 permissions_augment = ('ec2:DescribeTags',)
131 default_report_fields = (
132 'CustodianDate',
133 'InstanceId',
134 'tag:Name',
135 'InstanceType',
136 'LaunchTime',
137 'VpcId',
138 'PrivateIpAddress',
139 )
141 filter_registry = filters
142 action_registry = actions
144 # if we have to do a fallback scenario where tags don't come in describe
145 permissions = ('ec2:DescribeTags',)
146 source_mapping = {
147 'describe': DescribeEC2,
148 'config': query.ConfigSource
149 }
152@filters.register('security-group')
153class SecurityGroupFilter(net_filters.SecurityGroupFilter):
155 RelatedIdsExpression = "NetworkInterfaces[].Groups[].GroupId"
158@filters.register('subnet')
159class SubnetFilter(net_filters.SubnetFilter):
161 RelatedIdsExpression = "NetworkInterfaces[].SubnetId"
164@filters.register('vpc')
165class VpcFilter(net_filters.VpcFilter):
167 RelatedIdsExpression = "VpcId"
170@filters.register('check-permissions')
171class ComputePermissions(CheckPermissions):
173 def get_iam_arns(self, resources):
174 profile_arn_map = {
175 r['IamInstanceProfile']['Arn']: r['IamInstanceProfile']['Id']
176 for r in resources if 'IamInstanceProfile' in r}
178 # py2 compat on dict ordering
179 profile_arns = list(profile_arn_map.items())
180 profile_role_map = {
181 arn: profile['Roles'][0]['Arn']
182 for arn, profile in zip(
183 [p[0] for p in profile_arns],
184 self.manager.get_resource_manager(
185 'iam-profile').get_resources(
186 [p[0].split('/', 1)[-1] for p in profile_arns]))}
187 return [
188 profile_role_map.get(r.get('IamInstanceProfile', {}).get('Arn'))
189 for r in resources]
192@filters.register('state-age')
193class StateTransitionAge(AgeFilter):
194 """Age an instance has been in the given state.
196 .. code-block:: yaml
198 policies:
199 - name: ec2-state-running-7-days
200 resource: ec2
201 filters:
202 - type: state-age
203 op: ge
204 days: 7
205 """
206 RE_PARSE_AGE = re.compile(r"\(.*?\)")
208 # this filter doesn't use date_attribute, but needs to define it
209 # to pass AgeFilter's validate method
210 date_attribute = "dummy"
212 schema = type_schema(
213 'state-age',
214 op={'$ref': '#/definitions/filters_common/comparison_operators'},
215 days={'type': 'number'})
217 def get_resource_date(self, i):
218 v = i.get('StateTransitionReason')
219 if not v:
220 return None
221 dates = self.RE_PARSE_AGE.findall(v)
222 if dates:
223 return parse(dates[0][1:-1])
224 return None
227@filters.register('ebs')
228class AttachedVolume(ValueFilter):
229 """EC2 instances with EBS backed volume
231 Filters EC2 instances with EBS backed storage devices (non ephemeral)
233 :Example:
235 .. code-block:: yaml
237 policies:
238 - name: ec2-encrypted-ebs-volumes
239 resource: ec2
240 filters:
241 - type: ebs
242 key: Encrypted
243 value: true
244 """
246 schema = type_schema(
247 'ebs', rinherit=ValueFilter.schema,
248 **{'operator': {'enum': ['and', 'or']},
249 'skip-devices': {'type': 'array', 'items': {'type': 'string'}}})
250 schema_alias = False
252 def get_permissions(self):
253 return self.manager.get_resource_manager('ebs').get_permissions()
255 def process(self, resources, event=None):
256 self.volume_map = self.get_volume_mapping(resources)
257 self.skip = self.data.get('skip-devices', [])
258 self.operator = self.data.get(
259 'operator', 'or') == 'or' and any or all
260 return list(filter(self, resources))
262 def get_volume_mapping(self, resources):
263 volume_map = {}
264 manager = self.manager.get_resource_manager('ebs')
265 for instance_set in utils.chunks(resources, 200):
266 volume_ids = []
267 for i in instance_set:
268 for bd in i.get('BlockDeviceMappings', ()):
269 if 'Ebs' not in bd:
270 continue
271 volume_ids.append(bd['Ebs']['VolumeId'])
272 for v in manager.get_resources(volume_ids):
273 if not v['Attachments']:
274 continue
275 volume_map.setdefault(
276 v['Attachments'][0]['InstanceId'], []).append(v)
277 return volume_map
279 def __call__(self, i):
280 volumes = self.volume_map.get(i['InstanceId'])
281 if not volumes:
282 return False
283 if self.skip:
284 for v in list(volumes):
285 for a in v.get('Attachments', []):
286 if a['Device'] in self.skip:
287 volumes.remove(v)
288 return self.operator(map(self.match, volumes))
291@filters.register('stop-protected')
292class DisableApiStop(Filter):
293 """EC2 instances with ``disableApiStop`` attribute set
295 Filters EC2 instances with ``disableApiStop`` attribute set to true.
297 :Example:
299 .. code-block:: yaml
301 policies:
302 - name: stop-protection-enabled
303 resource: ec2
304 filters:
305 - type: stop-protected
307 :Example:
309 .. code-block:: yaml
311 policies:
312 - name: stop-protection-NOT-enabled
313 resource: ec2
314 filters:
315 - not:
316 - type: stop-protected
317 """
319 schema = type_schema('stop-protected')
320 permissions = ('ec2:DescribeInstanceAttribute',)
322 def process(self, resources: List[dict], event=None) -> List[dict]:
323 client = utils.local_session(
324 self.manager.session_factory).client('ec2')
325 return [r for r in resources
326 if self._is_stop_protection_enabled(client, r)]
328 def _is_stop_protection_enabled(self, client, instance: dict) -> bool:
329 attr_val = self.manager.retry(
330 client.describe_instance_attribute,
331 Attribute='disableApiStop',
332 InstanceId=instance['InstanceId']
333 )
334 return attr_val['DisableApiStop']['Value']
336 def validate(self) -> None:
337 botocore_min_version = '1.26.7'
339 if LooseVersion(botocore.__version__) < LooseVersion(botocore_min_version):
340 raise PolicyValidationError(
341 "'stop-protected' filter requires botocore version "
342 f'{botocore_min_version} or above. '
343 f'Installed version is {botocore.__version__}.'
344 )
347@filters.register('termination-protected')
348class DisableApiTermination(Filter):
349 """EC2 instances with ``disableApiTermination`` attribute set
351 Filters EC2 instances with ``disableApiTermination`` attribute set to true.
353 :Example:
355 .. code-block:: yaml
357 policies:
358 - name: termination-protection-enabled
359 resource: ec2
360 filters:
361 - type: termination-protected
363 :Example:
365 .. code-block:: yaml
367 policies:
368 - name: termination-protection-NOT-enabled
369 resource: ec2
370 filters:
371 - not:
372 - type: termination-protected
373 """
375 schema = type_schema('termination-protected')
376 permissions = ('ec2:DescribeInstanceAttribute',)
378 def get_permissions(self):
379 perms = list(self.permissions)
380 perms.extend(self.manager.get_permissions())
381 return perms
383 def process(self, resources, event=None):
384 client = utils.local_session(
385 self.manager.session_factory).client('ec2')
386 return [r for r in resources
387 if self.is_termination_protection_enabled(client, r)]
389 def is_termination_protection_enabled(self, client, inst):
390 attr_val = self.manager.retry(
391 client.describe_instance_attribute,
392 Attribute='disableApiTermination',
393 InstanceId=inst['InstanceId']
394 )
395 return attr_val['DisableApiTermination']['Value']
398class InstanceImageBase:
400 def prefetch_instance_images(self, instances):
401 image_ids = [i['ImageId'] for i in instances if 'c7n:instance-image' not in i]
402 self.image_map = self.get_local_image_mapping(image_ids)
404 def get_base_image_mapping(self):
405 return {i['ImageId']: i for i in
406 self.manager.get_resource_manager('ami').resources()}
408 def get_instance_image(self, instance):
409 image = instance.get('c7n:instance-image', None)
410 if not image:
411 image = instance['c7n:instance-image'] = self.image_map.get(instance['ImageId'], None)
412 return image
414 def get_local_image_mapping(self, image_ids):
415 base_image_map = self.get_base_image_mapping()
416 resources = {i: base_image_map[i] for i in image_ids if i in base_image_map}
417 missing = list(set(image_ids) - set(resources.keys()))
418 if missing:
419 loaded = self.manager.get_resource_manager('ami').get_resources(missing, False)
420 resources.update({image['ImageId']: image for image in loaded})
421 return resources
424@filters.register('image-age')
425class ImageAge(AgeFilter, InstanceImageBase):
426 """EC2 AMI age filter
428 Filters EC2 instances based on the age of their AMI image (in days)
430 :Example:
432 .. code-block:: yaml
434 policies:
435 - name: ec2-ancient-ami
436 resource: ec2
437 filters:
438 - type: image-age
439 op: ge
440 days: 90
441 """
443 date_attribute = "CreationDate"
445 schema = type_schema(
446 'image-age',
447 op={'$ref': '#/definitions/filters_common/comparison_operators'},
448 days={'type': 'number'})
450 def get_permissions(self):
451 return self.manager.get_resource_manager('ami').get_permissions()
453 def process(self, resources, event=None):
454 self.prefetch_instance_images(resources)
455 return super(ImageAge, self).process(resources, event)
457 def get_resource_date(self, i):
458 image = self.get_instance_image(i)
459 if image:
460 return parse(image['CreationDate'])
461 else:
462 return parse("2000-01-01T01:01:01.000Z")
465@filters.register('image')
466class InstanceImage(ValueFilter, InstanceImageBase):
468 schema = type_schema('image', rinherit=ValueFilter.schema)
469 schema_alias = False
471 def get_permissions(self):
472 return self.manager.get_resource_manager('ami').get_permissions()
474 def process(self, resources, event=None):
475 self.prefetch_instance_images(resources)
476 return super(InstanceImage, self).process(resources, event)
478 def __call__(self, i):
479 image = self.get_instance_image(i)
480 # Finally, if we have no image...
481 if not image:
482 self.log.warning(
483 "Could not locate image for instance:%s ami:%s" % (
484 i['InstanceId'], i["ImageId"]))
485 # Match instead on empty skeleton?
486 return False
487 return self.match(image)
490@filters.register('offhour')
491class InstanceOffHour(OffHour):
492 """Custodian OffHour filter
494 Filters running EC2 instances with the intent to stop at a given hour of
495 the day. A list of days to excluded can be included as a list of strings
496 with the format YYYY-MM-DD. Alternatively, the list (using the same syntax)
497 can be taken from a specified url.
499 Note: You can disable filtering of only running instances by setting
500 `state-filter: false`
502 :Example:
504 .. code-block:: yaml
506 policies:
507 - name: offhour-evening-stop
508 resource: ec2
509 filters:
510 - type: offhour
511 tag: custodian_downtime
512 default_tz: et
513 offhour: 20
514 actions:
515 - stop
517 - name: offhour-evening-stop-skip-holidays
518 resource: ec2
519 filters:
520 - type: offhour
521 tag: custodian_downtime
522 default_tz: et
523 offhour: 20
524 skip-days: ['2017-12-25']
525 actions:
526 - stop
528 - name: offhour-evening-stop-skip-holidays-from
529 resource: ec2
530 filters:
531 - type: offhour
532 tag: custodian_downtime
533 default_tz: et
534 offhour: 20
535 skip-days-from:
536 expr: 0
537 format: csv
538 url: 's3://location/holidays.csv'
539 actions:
540 - stop
541 """
543 schema = type_schema(
544 'offhour', rinherit=OffHour.schema,
545 **{'state-filter': {'type': 'boolean'}})
546 schema_alias = False
548 valid_origin_states = ('running',)
550 def process(self, resources, event=None):
551 if self.data.get('state-filter', True):
552 return super(InstanceOffHour, self).process(
553 self.filter_resources(resources, 'State.Name', self.valid_origin_states))
554 else:
555 return super(InstanceOffHour, self).process(resources)
558@filters.register('network-location')
559class EC2NetworkLocation(net_filters.NetworkLocation):
561 valid_origin_states = ('pending', 'running', 'shutting-down', 'stopping',
562 'stopped')
564 def process(self, resources, event=None):
565 resources = self.filter_resources(resources, 'State.Name', self.valid_origin_states)
566 if not resources:
567 return []
568 return super(EC2NetworkLocation, self).process(resources)
571@filters.register('onhour')
572class InstanceOnHour(OnHour):
573 """Custodian OnHour filter
575 Filters stopped EC2 instances with the intent to start at a given hour of
576 the day. A list of days to excluded can be included as a list of strings
577 with the format YYYY-MM-DD. Alternatively, the list (using the same syntax)
578 can be taken from a specified url.
580 Note: You can disable filtering of only stopped instances by setting
581 `state-filter: false`
583 :Example:
585 .. code-block:: yaml
587 policies:
588 - name: onhour-morning-start
589 resource: ec2
590 filters:
591 - type: onhour
592 tag: custodian_downtime
593 default_tz: et
594 onhour: 6
595 actions:
596 - start
598 - name: onhour-morning-start-skip-holidays
599 resource: ec2
600 filters:
601 - type: onhour
602 tag: custodian_downtime
603 default_tz: et
604 onhour: 6
605 skip-days: ['2017-12-25']
606 actions:
607 - start
609 - name: onhour-morning-start-skip-holidays-from
610 resource: ec2
611 filters:
612 - type: onhour
613 tag: custodian_downtime
614 default_tz: et
615 onhour: 6
616 skip-days-from:
617 expr: 0
618 format: csv
619 url: 's3://location/holidays.csv'
620 actions:
621 - start
622 """
624 schema = type_schema(
625 'onhour', rinherit=OnHour.schema,
626 **{'state-filter': {'type': 'boolean'}})
627 schema_alias = False
629 valid_origin_states = ('stopped',)
631 def process(self, resources, event=None):
632 if self.data.get('state-filter', True):
633 return super(InstanceOnHour, self).process(
634 self.filter_resources(resources, 'State.Name', self.valid_origin_states))
635 else:
636 return super(InstanceOnHour, self).process(resources)
639@filters.register('ephemeral')
640class EphemeralInstanceFilter(Filter):
641 """EC2 instances with ephemeral storage
643 Filters EC2 instances that have ephemeral storage (an instance-store backed
644 root device)
646 :Example:
648 .. code-block:: yaml
650 policies:
651 - name: ec2-ephemeral-instances
652 resource: ec2
653 filters:
654 - type: ephemeral
656 http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html
657 """
659 schema = type_schema('ephemeral')
661 def __call__(self, i):
662 return self.is_ephemeral(i)
664 @staticmethod
665 def is_ephemeral(i):
666 for bd in i.get('BlockDeviceMappings', []):
667 if bd['DeviceName'] in ('/dev/sda1', '/dev/xvda', 'xvda'):
668 if 'Ebs' in bd:
669 return False
670 return True
671 return True
674@filters.register('instance-uptime')
675class UpTimeFilter(AgeFilter):
677 date_attribute = "LaunchTime"
679 schema = type_schema(
680 'instance-uptime',
681 op={'$ref': '#/definitions/filters_common/comparison_operators'},
682 days={'type': 'number'})
685@filters.register('instance-age')
686class InstanceAgeFilter(AgeFilter):
687 """Filters instances based on their age (in days)
689 :Example:
691 .. code-block:: yaml
693 policies:
694 - name: ec2-30-days-plus
695 resource: ec2
696 filters:
697 - type: instance-age
698 op: ge
699 days: 30
700 """
702 date_attribute = "LaunchTime"
703 ebs_key_func = operator.itemgetter('AttachTime')
705 schema = type_schema(
706 'instance-age',
707 op={'$ref': '#/definitions/filters_common/comparison_operators'},
708 days={'type': 'number'},
709 hours={'type': 'number'},
710 minutes={'type': 'number'})
712 def get_resource_date(self, i):
713 # LaunchTime is basically how long has the instance
714 # been on, use the oldest ebs vol attach time
715 ebs_vols = [
716 block['Ebs'] for block in i['BlockDeviceMappings']
717 if 'Ebs' in block]
718 if not ebs_vols:
719 # Fall back to using age attribute (ephemeral instances)
720 return super(InstanceAgeFilter, self).get_resource_date(i)
721 # Lexographical sort on date
722 ebs_vols = sorted(ebs_vols, key=self.ebs_key_func)
723 return ebs_vols[0]['AttachTime']
726@filters.register('default-vpc')
727class DefaultVpc(net_filters.DefaultVpcBase):
728 """ Matches if an ec2 database is in the default vpc
729 """
731 schema = type_schema('default-vpc')
733 def __call__(self, ec2):
734 return ec2.get('VpcId') and self.match(ec2.get('VpcId')) or False
737def deserialize_user_data(user_data):
738 data = base64.b64decode(user_data)
739 # try raw and compressed
740 try:
741 return data.decode('utf8')
742 except UnicodeDecodeError:
743 return zlib.decompress(data, 16).decode('utf8')
746@filters.register('user-data')
747class UserData(ValueFilter):
748 """Filter on EC2 instances which have matching userdata.
749 Note: It is highly recommended to use regexes with the ?sm flags, since Custodian
750 uses re.match() and userdata spans multiple lines.
752 :example:
754 .. code-block:: yaml
756 policies:
757 - name: ec2_userdata_stop
758 resource: ec2
759 filters:
760 - type: user-data
761 op: regex
762 value: (?smi).*password=
763 actions:
764 - stop
765 """
767 schema = type_schema('user-data', rinherit=ValueFilter.schema)
768 schema_alias = False
769 batch_size = 50
770 annotation = 'c7n:user-data'
771 permissions = ('ec2:DescribeInstanceAttribute',)
773 def __init__(self, data, manager):
774 super(UserData, self).__init__(data, manager)
775 self.data['key'] = '"c7n:user-data"'
777 def process(self, resources, event=None):
778 client = utils.local_session(self.manager.session_factory).client('ec2')
779 results = []
780 with self.executor_factory(max_workers=3) as w:
781 futures = {}
782 for instance_set in utils.chunks(resources, self.batch_size):
783 futures[w.submit(
784 self.process_instance_set,
785 client, instance_set)] = instance_set
787 for f in as_completed(futures):
788 if f.exception():
789 self.log.error(
790 "Error processing userdata on instance set %s", f.exception())
791 results.extend(f.result())
792 return results
794 def process_instance_set(self, client, resources):
795 results = []
796 for r in resources:
797 if self.annotation not in r:
798 try:
799 result = client.describe_instance_attribute(
800 Attribute='userData',
801 InstanceId=r['InstanceId'])
802 except ClientError as e:
803 if e.response['Error']['Code'] == 'InvalidInstanceId.NotFound':
804 continue
805 if 'Value' not in result['UserData']:
806 r[self.annotation] = None
807 else:
808 r[self.annotation] = deserialize_user_data(
809 result['UserData']['Value'])
810 if self.match(r):
811 results.append(r)
812 return results
815@filters.register('singleton')
816class SingletonFilter(Filter):
817 """EC2 instances without autoscaling or a recover alarm
819 Filters EC2 instances that are not members of an autoscaling group
820 and do not have Cloudwatch recover alarms.
822 :Example:
824 .. code-block:: yaml
826 policies:
827 - name: ec2-recover-instances
828 resource: ec2
829 filters:
830 - singleton
831 actions:
832 - type: tag
833 key: problem
834 value: instance is not resilient
836 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-recover.html
837 """
839 schema = type_schema('singleton')
841 permissions = ('cloudwatch:DescribeAlarmsForMetric',)
843 valid_origin_states = ('running', 'stopped', 'pending', 'stopping')
845 in_asg = ValueFilter({
846 'key': 'tag:aws:autoscaling:groupName',
847 'value': 'not-null'}).validate()
849 def process(self, instances, event=None):
850 return super(SingletonFilter, self).process(
851 self.filter_resources(instances, 'State.Name', self.valid_origin_states))
853 def __call__(self, i):
854 if self.in_asg(i):
855 return False
856 else:
857 return not self.has_recover_alarm(i)
859 def has_recover_alarm(self, i):
860 client = utils.local_session(self.manager.session_factory).client('cloudwatch')
861 alarms = client.describe_alarms_for_metric(
862 MetricName='StatusCheckFailed_System',
863 Namespace='AWS/EC2',
864 Dimensions=[
865 {
866 'Name': 'InstanceId',
867 'Value': i['InstanceId']
868 }
869 ]
870 )
872 for i in alarms['MetricAlarms']:
873 for a in i['AlarmActions']:
874 if (
875 a.startswith('arn:aws:automate:') and
876 a.endswith(':ec2:recover')
877 ):
878 return True
880 return False
883@EC2.filter_registry.register('ssm')
884class SsmStatus(ValueFilter):
885 """Filter ec2 instances by their ssm status information.
887 :Example:
889 Find ubuntu 18.04 instances are active with ssm.
891 .. code-block:: yaml
893 policies:
894 - name: ec2-ssm-check
895 resource: ec2
896 filters:
897 - type: ssm
898 key: PingStatus
899 value: Online
900 - type: ssm
901 key: PlatformName
902 value: Ubuntu
903 - type: ssm
904 key: PlatformVersion
905 value: 18.04
906 """
907 schema = type_schema('ssm', rinherit=ValueFilter.schema)
908 schema_alias = False
909 permissions = ('ssm:DescribeInstanceInformation',)
910 annotation = 'c7n:SsmState'
912 def process(self, resources, event=None):
913 client = utils.local_session(self.manager.session_factory).client('ssm')
914 results = []
915 for resource_set in utils.chunks(
916 [r for r in resources if self.annotation not in r], 50):
917 self.process_resource_set(client, resource_set)
918 for r in resources:
919 if self.match(r[self.annotation]):
920 results.append(r)
921 return results
923 def process_resource_set(self, client, resources):
924 instance_ids = [i['InstanceId'] for i in resources]
925 info_map = {
926 info['InstanceId']: info for info in
927 client.describe_instance_information(
928 Filters=[{'Key': 'InstanceIds', 'Values': instance_ids}]).get(
929 'InstanceInformationList', [])}
930 for r in resources:
931 r[self.annotation] = info_map.get(r['InstanceId'], {})
934@EC2.filter_registry.register('ssm-inventory')
935class SsmInventory(Filter):
936 """Filter EC2 instances by their SSM software inventory.
938 :Example:
940 Find instances that have a specific package installed.
942 .. code-block:: yaml
944 policies:
945 - name: ec2-find-specific-package
946 resource: ec2
947 filters:
948 - type: ssm-inventory
949 query:
950 - Key: Name
951 Values:
952 - "docker"
953 Type: Equal
955 - name: ec2-get-all-packages
956 resource: ec2
957 filters:
958 - type: ssm-inventory
959 """
960 schema = type_schema(
961 'ssm-inventory',
962 **{'query': {'type': 'array', 'items': {
963 'type': 'object',
964 'properties': {
965 'Key': {'type': 'string'},
966 'Values': {'type': 'array', 'items': {'type': 'string'}},
967 'Type': {'enum': ['Equal', 'NotEqual', 'BeginWith', 'LessThan',
968 'GreaterThan', 'Exists']}},
969 'required': ['Key', 'Values']}}})
971 permissions = ('ssm:ListInventoryEntries',)
972 annotation_key = 'c7n:SSM-Inventory'
974 def process(self, resources, event=None):
975 client = utils.local_session(self.manager.session_factory).client('ssm')
976 query = self.data.get("query")
977 found = []
978 for r in resources:
979 entries = []
980 next_token = None
981 while True:
982 params = {
983 "InstanceId": r["InstanceId"],
984 "TypeName": "AWS:Application"
985 }
986 if next_token:
987 params['NextToken'] = next_token
988 if query:
989 params['Filters'] = query
990 response = client.list_inventory_entries(**params)
991 all_entries = response["Entries"]
992 if all_entries:
993 entries.extend(all_entries)
994 next_token = response.get('NextToken')
995 if not next_token:
996 break
997 if entries:
998 r[self.annotation_key] = entries
999 found.append(r)
1000 return found
1003@EC2.filter_registry.register('ssm-compliance')
1004class SsmCompliance(Filter):
1005 """Filter ec2 instances by their ssm compliance status.
1007 :Example:
1009 Find non-compliant ec2 instances.
1011 .. code-block:: yaml
1013 policies:
1014 - name: ec2-ssm-compliance
1015 resource: ec2
1016 filters:
1017 - type: ssm-compliance
1018 compliance_types:
1019 - Association
1020 - Patch
1021 severity:
1022 - CRITICAL
1023 - HIGH
1024 - MEDIUM
1025 - LOW
1026 - UNSPECIFIED
1027 states:
1028 - NON_COMPLIANT
1029 eval_filters:
1030 - type: value
1031 key: ExecutionSummary.ExecutionTime
1032 value_type: age
1033 value: 30
1034 op: less-than
1035 """
1036 schema = type_schema(
1037 'ssm-compliance',
1038 **{'required': ['compliance_types'],
1039 'compliance_types': {'type': 'array', 'items': {'type': 'string'}},
1040 'severity': {'type': 'array', 'items': {'type': 'string'}},
1041 'op': {'enum': ['or', 'and']},
1042 'eval_filters': {'type': 'array', 'items': {
1043 'oneOf': [
1044 {'$ref': '#/definitions/filters/valuekv'},
1045 {'$ref': '#/definitions/filters/value'}]}},
1046 'states': {'type': 'array',
1047 'default': ['NON_COMPLIANT'],
1048 'items': {
1049 'enum': [
1050 'COMPLIANT',
1051 'NON_COMPLIANT'
1052 ]}}})
1053 permissions = ('ssm:ListResourceComplianceSummaries',)
1054 annotation = 'c7n:ssm-compliance'
1056 def process(self, resources, event=None):
1057 op = self.data.get('op', 'or') == 'or' and any or all
1058 eval_filters = []
1059 for f in self.data.get('eval_filters', ()):
1060 vf = ValueFilter(f)
1061 vf.annotate = False
1062 eval_filters.append(vf)
1064 client = utils.local_session(self.manager.session_factory).client('ssm')
1065 filters = [
1066 {
1067 'Key': 'Status',
1068 'Values': self.data['states'],
1069 'Type': 'EQUAL'
1070 },
1071 {
1072 'Key': 'ComplianceType',
1073 'Values': self.data['compliance_types'],
1074 'Type': 'EQUAL'
1075 }
1076 ]
1077 severity = self.data.get('severity')
1078 if severity:
1079 filters.append(
1080 {
1081 'Key': 'OverallSeverity',
1082 'Values': severity,
1083 'Type': 'EQUAL'
1084 })
1086 resource_map = {}
1087 pager = client.get_paginator('list_resource_compliance_summaries')
1088 for page in pager.paginate(Filters=filters):
1089 items = page['ResourceComplianceSummaryItems']
1090 for i in items:
1091 if not eval_filters:
1092 resource_map.setdefault(
1093 i['ResourceId'], []).append(i)
1094 continue
1095 if op([f.match(i) for f in eval_filters]):
1096 resource_map.setdefault(
1097 i['ResourceId'], []).append(i)
1099 results = []
1100 for r in resources:
1101 result = resource_map.get(r['InstanceId'])
1102 if result:
1103 r[self.annotation] = result
1104 results.append(r)
1106 return results
1109@actions.register('set-monitoring')
1110class MonitorInstances(BaseAction):
1111 """Action on EC2 Instances to enable/disable detailed monitoring
1113 The different states of detailed monitoring status are :
1114 'disabled'|'disabling'|'enabled'|'pending'
1115 (https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.describe_instances)
1117 :Example:
1119 .. code-block:: yaml
1121 policies:
1122 - name: ec2-detailed-monitoring-activation
1123 resource: ec2
1124 filters:
1125 - Monitoring.State: disabled
1126 actions:
1127 - type: set-monitoring
1128 state: enable
1130 References
1132 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-cloudwatch-new.html
1133 """
1134 schema = type_schema('set-monitoring',
1135 **{'state': {'enum': ['enable', 'disable']}})
1136 permissions = ('ec2:MonitorInstances', 'ec2:UnmonitorInstances')
1138 def process(self, resources, event=None):
1139 client = utils.local_session(
1140 self.manager.session_factory).client('ec2')
1141 actions = {
1142 'enable': self.enable_monitoring,
1143 'disable': self.disable_monitoring
1144 }
1145 for instances_set in utils.chunks(resources, 20):
1146 actions[self.data.get('state')](client, instances_set)
1148 def enable_monitoring(self, client, resources):
1149 try:
1150 client.monitor_instances(
1151 InstanceIds=[inst['InstanceId'] for inst in resources]
1152 )
1153 except ClientError as e:
1154 if e.response['Error']['Code'] != 'InvalidInstanceId.NotFound':
1155 raise
1157 def disable_monitoring(self, client, resources):
1158 try:
1159 client.unmonitor_instances(
1160 InstanceIds=[inst['InstanceId'] for inst in resources]
1161 )
1162 except ClientError as e:
1163 if e.response['Error']['Code'] != 'InvalidInstanceId.NotFound':
1164 raise
1167@EC2.action_registry.register('set-metadata-access')
1168class SetMetadataServerAccess(BaseAction):
1169 """Set instance metadata server access for an instance.
1171 :example:
1173 Require instances to use IMDSv2
1175 .. code-block:: yaml
1177 policies:
1178 - name: ec2-require-imdsv2
1179 resource: ec2
1180 filters:
1181 - MetadataOptions.HttpTokens: optional
1182 actions:
1183 - type: set-metadata-access
1184 tokens: required
1186 :example:
1188 Disable metadata server access
1190 .. code-block: yaml
1192 policies:
1193 - name: ec2-disable-imds
1194 resource: ec2
1195 filters:
1196 - MetadataOptions.HttpEndpoint: enabled
1197 actions:
1198 - type: set-metadata-access
1199 endpoint: disabled
1201 policies:
1202 - name: ec2-enable-metadata-tags
1203 resource: ec2
1204 filters:
1205 - MetadataOptions.InstanceMetadataTags: disabled
1206 actions:
1207 - type: set-metadata-access
1208 metadata-tags: enabled
1210 Reference: https://amzn.to/2XOuxpQ
1211 """
1213 AllowedValues = {
1214 'HttpEndpoint': ['enabled', 'disabled'],
1215 'HttpTokens': ['required', 'optional'],
1216 'InstanceMetadataTags': ['enabled', 'disabled'],
1217 'HttpPutResponseHopLimit': list(range(1, 65))
1218 }
1220 schema = type_schema(
1221 'set-metadata-access',
1222 anyOf=[{'required': ['endpoint']},
1223 {'required': ['tokens']},
1224 {'required': ['metadatatags']},
1225 {'required': ['hop-limit']}],
1226 **{'endpoint': {'enum': AllowedValues['HttpEndpoint']},
1227 'tokens': {'enum': AllowedValues['HttpTokens']},
1228 'metadata-tags': {'enum': AllowedValues['InstanceMetadataTags']},
1229 'hop-limit': {'type': 'integer', 'minimum': 1, 'maximum': 64}}
1230 )
1231 permissions = ('ec2:ModifyInstanceMetadataOptions',)
1233 def get_params(self):
1234 return filter_empty({
1235 'HttpEndpoint': self.data.get('endpoint'),
1236 'HttpTokens': self.data.get('tokens'),
1237 'InstanceMetadataTags': self.data.get('metadata-tags'),
1238 'HttpPutResponseHopLimit': self.data.get('hop-limit')})
1240 def process(self, resources):
1241 params = self.get_params()
1242 for k, v in params.items():
1243 allowed_values = list(self.AllowedValues[k])
1244 allowed_values.remove(v)
1245 resources = self.filter_resources(
1246 resources, 'MetadataOptions.%s' % k, allowed_values)
1248 if not resources:
1249 return
1251 client = utils.local_session(self.manager.session_factory).client('ec2')
1252 for r in resources:
1253 self.manager.retry(
1254 client.modify_instance_metadata_options,
1255 ignore_err_codes=('InvalidInstanceId.NotFound',),
1256 InstanceId=r['InstanceId'],
1257 **params)
1260@EC2.action_registry.register("post-finding")
1261class InstanceFinding(PostFinding):
1263 resource_type = 'AwsEc2Instance'
1265 def format_resource(self, r):
1266 ip_addresses = jmespath_search(
1267 "NetworkInterfaces[].PrivateIpAddresses[].PrivateIpAddress", r)
1269 # limit to max 10 ip addresses, per security hub service limits
1270 ip_addresses = ip_addresses and ip_addresses[:10] or ip_addresses
1271 details = {
1272 "Type": r["InstanceType"],
1273 "ImageId": r["ImageId"],
1274 "IpV4Addresses": ip_addresses,
1275 "KeyName": r.get("KeyName"),
1276 "LaunchedAt": r["LaunchTime"].isoformat()
1277 }
1279 if "VpcId" in r:
1280 details["VpcId"] = r["VpcId"]
1281 if "SubnetId" in r:
1282 details["SubnetId"] = r["SubnetId"]
1283 # config will use an empty key
1284 if "IamInstanceProfile" in r and r['IamInstanceProfile']:
1285 details["IamInstanceProfileArn"] = r["IamInstanceProfile"]["Arn"]
1287 instance = {
1288 "Type": self.resource_type,
1289 "Id": "arn:{}:ec2:{}:{}:instance/{}".format(
1290 utils.REGION_PARTITION_MAP.get(self.manager.config.region, 'aws'),
1291 self.manager.config.region,
1292 self.manager.config.account_id,
1293 r["InstanceId"]),
1294 "Region": self.manager.config.region,
1295 "Tags": {t["Key"]: t["Value"] for t in r.get("Tags", [])},
1296 "Details": {self.resource_type: filter_empty(details)},
1297 }
1299 instance = filter_empty(instance)
1300 return instance
1303@actions.register('start')
1304class Start(BaseAction):
1305 """Starts a previously stopped EC2 instance.
1307 :Example:
1309 .. code-block:: yaml
1311 policies:
1312 - name: ec2-start-stopped-instances
1313 resource: ec2
1314 query:
1315 - instance-state-name: stopped
1316 actions:
1317 - start
1319 http://docs.aws.amazon.com/cli/latest/reference/ec2/start-instances.html
1320 """
1322 valid_origin_states = ('stopped',)
1323 schema = type_schema('start')
1324 permissions = ('ec2:StartInstances',)
1325 batch_size = 10
1326 exception = None
1328 def _filter_ec2_with_volumes(self, instances):
1329 return [i for i in instances if len(i['BlockDeviceMappings']) > 0]
1331 def process(self, instances):
1332 instances = self._filter_ec2_with_volumes(
1333 self.filter_resources(instances, 'State.Name', self.valid_origin_states))
1334 if not len(instances):
1335 return
1337 client = utils.local_session(self.manager.session_factory).client('ec2')
1338 failures = {}
1340 # Play nice around aws having insufficient capacity...
1341 for itype, t_instances in utils.group_by(
1342 instances, 'InstanceType').items():
1343 for izone, z_instances in utils.group_by(
1344 t_instances, 'Placement.AvailabilityZone').items():
1345 for batch in utils.chunks(z_instances, self.batch_size):
1346 fails = self.process_instance_set(client, batch, itype, izone)
1347 if fails:
1348 failures["%s %s" % (itype, izone)] = [i['InstanceId'] for i in batch]
1350 if failures:
1351 fail_count = sum(map(len, failures.values()))
1352 msg = "Could not start %d of %d instances %s" % (
1353 fail_count, len(instances), utils.dumps(failures))
1354 self.log.warning(msg)
1355 raise RuntimeError(msg)
1357 def process_instance_set(self, client, instances, itype, izone):
1358 # Setup retry with insufficient capacity as well
1359 retryable = ('InsufficientInstanceCapacity', 'RequestLimitExceeded',
1360 'Client.RequestLimitExceeded', 'Server.InsufficientInstanceCapacity'),
1361 retry = utils.get_retry(retryable, max_attempts=5)
1362 instance_ids = [i['InstanceId'] for i in instances]
1363 while instance_ids:
1364 try:
1365 retry(client.start_instances, InstanceIds=instance_ids)
1366 break
1367 except ClientError as e:
1368 if e.response['Error']['Code'] in retryable:
1369 # we maxed out on our retries
1370 return True
1371 elif e.response['Error']['Code'] == 'IncorrectInstanceState':
1372 instance_ids.remove(extract_instance_id(e))
1373 else:
1374 raise
1377def extract_instance_id(state_error):
1378 "Extract an instance id from an error"
1379 instance_id = None
1380 match = RE_ERROR_INSTANCE_ID.search(str(state_error))
1381 if match:
1382 instance_id = match.groupdict().get('instance_id')
1383 if match is None or instance_id is None:
1384 raise ValueError("Could not extract instance id from error: %s" % state_error)
1385 return instance_id
1388@actions.register('resize')
1389class Resize(BaseAction):
1390 """Change an instance's size.
1392 An instance can only be resized when its stopped, this action
1393 can optionally stop/start an instance if needed to effect the instance
1394 type change. Instances are always left in the run state they were
1395 found in.
1397 There are a few caveats to be aware of, instance resizing
1398 needs to maintain compatibility for architecture, virtualization type
1399 hvm/pv, and ebs optimization at minimum.
1401 http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-resize.html
1403 This action also has specific support for enacting recommendations
1404 from the AWS Cost Optimization Hub for resizing.
1406 :example:
1408 .. code-block:: yaml
1410 policies:
1411 - name: ec2-rightsize
1412 resource: aws.ec2
1413 filters:
1414 - type: cost-optimization
1415 attrs:
1416 - actionType: Rightsize
1417 actions:
1418 - resize
1420 """
1422 schema = type_schema(
1423 'resize',
1424 **{'restart': {'type': 'boolean'},
1425 'type-map': {'type': 'object'},
1426 'default': {'type': 'string'}})
1428 valid_origin_states = ('running', 'stopped')
1430 def get_permissions(self):
1431 perms = ('ec2:DescribeInstances', 'ec2:ModifyInstanceAttribute')
1432 if self.data.get('restart', False):
1433 perms += ('ec2:StopInstances', 'ec2:StartInstances')
1434 return perms
1436 def process(self, resources):
1437 stopped_instances = self.filter_resources(resources, 'State.Name', ('stopped',))
1438 running_instances = self.filter_resources(resources, 'State.Name', ('running',))
1440 if self.data.get('restart') and running_instances:
1441 Stop({'terminate-ephemeral': False},
1442 self.manager).process(running_instances)
1443 client = utils.local_session(
1444 self.manager.session_factory).client('ec2')
1445 waiter = client.get_waiter('instance_stopped')
1446 try:
1447 waiter.wait(
1448 InstanceIds=[r['InstanceId'] for r in running_instances])
1449 except ClientError as e:
1450 self.log.exception(
1451 "Exception stopping instances for resize:\n %s" % e)
1453 client = utils.local_session(self.manager.session_factory).client('ec2')
1455 for instance_set in utils.chunks(itertools.chain(
1456 stopped_instances, running_instances), 20):
1457 self.process_resource_set(instance_set, client)
1459 if self.data.get('restart') and running_instances:
1460 client.start_instances(
1461 InstanceIds=[i['InstanceId'] for i in running_instances])
1462 return list(itertools.chain(stopped_instances, running_instances))
1464 def process_resource_set(self, instance_set, client):
1466 for i in instance_set:
1467 new_type = self.get_target_instance_type(i)
1468 self.log.debug(
1469 "resizing %s %s -> %s" % (i['InstanceId'], i['InstanceType'], new_type)
1470 )
1472 if not new_type or new_type == i['InstanceType']:
1473 continue
1474 try:
1475 client.modify_instance_attribute(
1476 InstanceId=i['InstanceId'],
1477 InstanceType={'Value': new_type})
1478 except ClientError as e:
1479 self.log.exception(
1480 "Exception resizing instance:%s new:%s old:%s \n %s" % (
1481 i['InstanceId'], new_type, i['InstanceType'], e))
1483 def get_target_instance_type(self, i):
1484 optimizer_recommend = i.get(CostHubRecommendation.annotation_key)
1485 if optimizer_recommend and optimizer_recommend['actionType'] == 'Rightsize':
1486 return optimizer_recommend['recommendedResourceSummary']
1487 type_map = self.data.get('type-map', {})
1488 default_type = self.data.get('default')
1489 return type_map.get(i['InstanceType'], default_type)
1492@actions.register('stop')
1493class Stop(BaseAction):
1494 """Stops or hibernates a running EC2 instances
1496 :Example:
1498 .. code-block:: yaml
1500 policies:
1501 - name: ec2-stop-running-instances
1502 resource: ec2
1503 query:
1504 - instance-state-name: running
1505 actions:
1506 - stop
1508 - name: ec2-hibernate-instances
1509 resources: ec2
1510 query:
1511 - instance-state-name: running
1512 actions:
1513 - type: stop
1514 hibernate: true
1517 Note when using hiberate, instances not configured for hiberation
1518 will just be stopped.
1519 """
1520 valid_origin_states = ('running',)
1522 schema = type_schema(
1523 'stop',
1524 **{
1525 "terminate-ephemeral": {"type": "boolean"},
1526 "hibernate": {"type": "boolean"},
1527 "force": {"type": "boolean"},
1528 },
1529 )
1531 has_hibernate = jmespath_compile('[].HibernationOptions.Configured')
1533 def get_permissions(self):
1534 perms = ('ec2:StopInstances',)
1535 if self.data.get('terminate-ephemeral', False):
1536 perms += ('ec2:TerminateInstances',)
1537 if self.data.get("force"):
1538 perms += ("ec2:ModifyInstanceAttribute",)
1539 return perms
1541 def split_on_storage(self, instances):
1542 ephemeral = []
1543 persistent = []
1544 for i in instances:
1545 if EphemeralInstanceFilter.is_ephemeral(i):
1546 ephemeral.append(i)
1547 else:
1548 persistent.append(i)
1549 return ephemeral, persistent
1551 def split_on_hibernate(self, instances):
1552 enabled, disabled = [], []
1553 for status, i in zip(self.has_hibernate.search(instances), instances):
1554 if status is True:
1555 enabled.append(i)
1556 else:
1557 disabled.append(i)
1558 return enabled, disabled
1560 def process(self, instances):
1561 instances = self.filter_resources(instances, 'State.Name', self.valid_origin_states)
1562 if not len(instances):
1563 return
1564 client = utils.local_session(
1565 self.manager.session_factory).client('ec2')
1566 # Ephemeral instance can't be stopped.
1567 ephemeral, persistent = self.split_on_storage(instances)
1568 if self.data.get('terminate-ephemeral', False) and ephemeral:
1569 self._run_instances_op(client, 'terminate', ephemeral)
1570 if persistent:
1571 if self.data.get('hibernate', False):
1572 enabled, persistent = self.split_on_hibernate(persistent)
1573 if enabled:
1574 self._run_instances_op(client, 'stop', enabled, Hibernate=True)
1575 self._run_instances_op(client, 'stop', persistent)
1576 return instances
1578 def disable_protection(self, client, op, instances):
1579 def modify_instance(i, attribute):
1580 try:
1581 self.manager.retry(
1582 client.modify_instance_attribute,
1583 InstanceId=i['InstanceId'],
1584 Attribute=attribute,
1585 Value='false',
1586 )
1587 except ClientError as e:
1588 if e.response['Error']['Code'] == 'IncorrectInstanceState':
1589 return
1590 raise
1592 def process_instance(i, op):
1593 modify_instance(i, 'disableApiStop')
1594 if op == 'terminate':
1595 modify_instance(i, 'disableApiTermination')
1597 with self.executor_factory(max_workers=2) as w:
1598 list(w.map(process_instance, instances, [op] * len(instances)))
1600 def _run_instances_op(self, client, op, instances, **kwargs):
1601 client_op = client.stop_instances
1602 if op == 'terminate':
1603 client_op = client.terminate_instances
1605 instance_ids = [i['InstanceId'] for i in instances]
1607 while instances:
1608 try:
1609 return self.manager.retry(client_op, InstanceIds=instance_ids, **kwargs)
1610 except ClientError as e:
1611 if e.response['Error']['Code'] == 'IncorrectInstanceState':
1612 instance_ids.remove(extract_instance_id(e))
1613 if (
1614 e.response['Error']['Code'] == 'OperationNotPermitted' and
1615 self.data.get('force')
1616 ):
1617 self.log.info("Disabling stop and termination protection on instances")
1618 self.disable_protection(
1619 client,
1620 op,
1621 [i for i in instances if i.get('InstanceLifecycle') != 'spot'],
1622 )
1623 continue
1624 raise
1627@actions.register('reboot')
1628class Reboot(BaseAction):
1629 """Reboots a previously running EC2 instance.
1631 :Example:
1633 .. code-block:: yaml
1635 policies:
1636 - name: ec2-reboot-instances
1637 resource: ec2
1638 query:
1639 - instance-state-name: running
1640 actions:
1641 - reboot
1643 http://docs.aws.amazon.com/cli/latest/reference/ec2/reboot-instances.html
1644 """
1646 valid_origin_states = ('running',)
1647 schema = type_schema('reboot')
1648 permissions = ('ec2:RebootInstances',)
1649 batch_size = 10
1650 exception = None
1652 def _filter_ec2_with_volumes(self, instances):
1653 return [i for i in instances if len(i['BlockDeviceMappings']) > 0]
1655 def process(self, instances):
1656 instances = self._filter_ec2_with_volumes(
1657 self.filter_resources(instances, 'State.Name', self.valid_origin_states))
1658 if not len(instances):
1659 return
1661 client = utils.local_session(self.manager.session_factory).client('ec2')
1662 failures = {}
1664 for batch in utils.chunks(instances, self.batch_size):
1665 fails = self.process_instance_set(client, batch)
1666 if fails:
1667 failures = [i['InstanceId'] for i in batch]
1669 if failures:
1670 fail_count = sum(map(len, failures.values()))
1671 msg = "Could not reboot %d of %d instances %s" % (
1672 fail_count, len(instances),
1673 utils.dumps(failures))
1674 self.log.warning(msg)
1675 raise RuntimeError(msg)
1677 def process_instance_set(self, client, instances):
1678 # Setup retry with insufficient capacity as well
1679 retryable = ('InsufficientInstanceCapacity', 'RequestLimitExceeded',
1680 'Client.RequestLimitExceeded'),
1681 retry = utils.get_retry(retryable, max_attempts=5)
1682 instance_ids = [i['InstanceId'] for i in instances]
1683 try:
1684 retry(client.reboot_instances, InstanceIds=instance_ids)
1685 except ClientError as e:
1686 if e.response['Error']['Code'] in retryable:
1687 return True
1688 raise
1691@actions.register('terminate')
1692class Terminate(BaseAction):
1693 """ Terminate a set of instances.
1695 While ec2 offers a bulk delete api, any given instance can be configured
1696 with api deletion termination protection, so we can't use the bulk call
1697 reliabily, we need to process the instances individually. Additionally
1698 If we're configured with 'force' then we'll turn off instance termination
1699 and stop protection.
1701 :Example:
1703 .. code-block:: yaml
1705 policies:
1706 - name: ec2-process-termination
1707 resource: ec2
1708 filters:
1709 - type: marked-for-op
1710 op: terminate
1711 actions:
1712 - terminate
1713 """
1715 valid_origin_states = ('running', 'stopped', 'pending', 'stopping')
1717 schema = type_schema('terminate', force={'type': 'boolean'})
1719 def get_permissions(self):
1720 permissions = ("ec2:TerminateInstances",)
1721 if self.data.get('force'):
1722 permissions += ('ec2:ModifyInstanceAttribute',)
1723 return permissions
1725 def process_terminate(self, instances):
1726 client = utils.local_session(
1727 self.manager.session_factory).client('ec2')
1728 try:
1729 self.manager.retry(
1730 client.terminate_instances,
1731 InstanceIds=[i['InstanceId'] for i in instances])
1732 return
1733 except ClientError as e:
1734 if e.response['Error']['Code'] != 'OperationNotPermitted':
1735 raise
1736 if not self.data.get('force'):
1737 raise
1739 self.log.info("Disabling stop and termination protection on instances")
1740 self.disable_deletion_protection(
1741 client,
1742 [i for i in instances if i.get('InstanceLifecycle') != 'spot'])
1743 self.manager.retry(
1744 client.terminate_instances,
1745 InstanceIds=[i['InstanceId'] for i in instances])
1747 def process(self, instances):
1748 instances = self.filter_resources(instances, 'State.Name', self.valid_origin_states)
1749 if not len(instances):
1750 return
1751 # limit batch sizes to avoid api limits
1752 for batch in utils.chunks(instances, 100):
1753 self.process_terminate(batch)
1755 def disable_deletion_protection(self, client, instances):
1757 def modify_instance(i, attribute):
1758 try:
1759 self.manager.retry(
1760 client.modify_instance_attribute,
1761 InstanceId=i['InstanceId'],
1762 Attribute=attribute,
1763 Value='false')
1764 except ClientError as e:
1765 if e.response['Error']['Code'] == 'IncorrectInstanceState':
1766 return
1767 raise
1769 def process_instance(i):
1770 modify_instance(i, 'disableApiTermination')
1771 modify_instance(i, 'disableApiStop')
1773 with self.executor_factory(max_workers=2) as w:
1774 list(w.map(process_instance, instances))
1777@actions.register('snapshot')
1778class Snapshot(BaseAction):
1779 """Snapshot the volumes attached to an EC2 instance.
1781 Tags may be optionally added to the snapshot during creation.
1783 - `copy-volume-tags` copies all the tags from the specified
1784 volume to the corresponding snapshot.
1785 - `copy-tags` copies the listed tags from each volume
1786 to the snapshot. This is mutually exclusive with
1787 `copy-volume-tags`.
1788 - `tags` allows new tags to be added to each snapshot when using
1789 'copy-tags`. If no tags are specified, then the tag
1790 `custodian_snapshot` is added.
1792 The default behavior is `copy-volume-tags: true`.
1794 :Example:
1796 .. code-block:: yaml
1798 policies:
1799 - name: ec2-snapshots
1800 resource: ec2
1801 actions:
1802 - type: snapshot
1803 copy-tags:
1804 - Name
1805 tags:
1806 custodian_snapshot: True
1807 """
1809 schema = type_schema(
1810 'snapshot',
1811 **{'copy-tags': {'type': 'array', 'items': {'type': 'string'}},
1812 'copy-volume-tags': {'type': 'boolean'},
1813 'tags': {'type': 'object'},
1814 'exclude-boot': {'type': 'boolean', 'default': False}})
1815 permissions = ('ec2:CreateSnapshot', 'ec2:CreateTags',)
1817 def validate(self):
1818 if self.data.get('copy-tags') and 'copy-volume-tags' in self.data:
1819 raise PolicyValidationError(
1820 "Can specify copy-tags or copy-volume-tags, not both")
1822 def process(self, resources):
1823 client = utils.local_session(self.manager.session_factory).client('ec2')
1824 err = None
1825 with self.executor_factory(max_workers=2) as w:
1826 futures = {}
1827 for resource in resources:
1828 futures[w.submit(
1829 self.process_volume_set, client, resource)] = resource
1830 for f in as_completed(futures):
1831 if f.exception():
1832 err = f.exception()
1833 resource = futures[f]
1834 self.log.error(
1835 "Exception creating snapshot set instance:%s \n %s" % (
1836 resource['InstanceId'], err))
1837 if err:
1838 raise err
1840 def get_instance_name(self, resource):
1841 tags = resource.get('Tags', [])
1842 for tag in tags:
1843 if tag['Key'] == 'Name':
1844 return tag['Value']
1845 return "-"
1847 def process_volume_set(self, client, resource):
1848 i_name = self.get_instance_name(resource)
1849 params = dict(
1850 Description=f"Snapshot Created for {resource['InstanceId']} ({i_name})",
1851 InstanceSpecification={
1852 'ExcludeBootVolume': self.data.get('exclude-boot', False),
1853 'InstanceId': resource['InstanceId']})
1854 if 'copy-tags' in self.data:
1855 params['TagSpecifications'] = [{
1856 'ResourceType': 'snapshot',
1857 'Tags': self.get_snapshot_tags(resource)}]
1858 elif self.data.get('copy-volume-tags', True):
1859 params['CopyTagsFromSource'] = 'volume'
1861 try:
1862 result = self.manager.retry(client.create_snapshots, **params)
1863 resource['c7n:snapshots'] = [
1864 s['SnapshotId'] for s in result['Snapshots']]
1865 except ClientError as e:
1866 err_code = e.response['Error']['Code']
1867 if err_code not in (
1868 'InvalidInstanceId.NotFound',
1869 'ConcurrentSnapshotLimitExceeded',
1870 'IncorrectState'):
1871 raise
1872 self.log.warning(
1873 "action:snapshot instance:%s error:%s",
1874 resource['InstanceId'], err_code)
1876 def get_snapshot_tags(self, resource):
1877 user_tags = self.data.get('tags', {}) or {'custodian_snapshot': ''}
1878 copy_tags = self.data.get('copy-tags', [])
1879 return coalesce_copy_user_tags(resource, copy_tags, user_tags)
1882@actions.register('modify-security-groups')
1883class EC2ModifyVpcSecurityGroups(ModifyVpcSecurityGroupsAction):
1884 """Modify security groups on an instance."""
1886 permissions = ("ec2:ModifyNetworkInterfaceAttribute",)
1887 sg_expr = jmespath_compile("Groups[].GroupId")
1889 def process(self, instances):
1890 if not len(instances):
1891 return
1892 client = utils.local_session(
1893 self.manager.session_factory).client('ec2')
1895 # handle multiple ENIs
1896 interfaces = []
1897 for i in instances:
1898 for eni in i['NetworkInterfaces']:
1899 if i.get('c7n:matched-security-groups'):
1900 eni['c7n:matched-security-groups'] = i[
1901 'c7n:matched-security-groups']
1902 if i.get('c7n:NetworkLocation'):
1903 eni['c7n:NetworkLocation'] = i[
1904 'c7n:NetworkLocation']
1905 interfaces.append(eni)
1907 groups = super(EC2ModifyVpcSecurityGroups, self).get_groups(interfaces)
1909 for idx, i in enumerate(interfaces):
1910 client.modify_network_interface_attribute(
1911 NetworkInterfaceId=i['NetworkInterfaceId'],
1912 Groups=groups[idx])
1915@actions.register('autorecover-alarm')
1916class AutorecoverAlarm(BaseAction):
1917 """Adds a cloudwatch metric alarm to recover an EC2 instance.
1919 This action takes effect on instances that are NOT part
1920 of an ASG.
1922 :Example:
1924 .. code-block:: yaml
1926 policies:
1927 - name: ec2-autorecover-alarm
1928 resource: ec2
1929 filters:
1930 - singleton
1931 actions:
1932 - autorecover-alarm
1934 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-recover.html
1935 """
1937 schema = type_schema('autorecover-alarm')
1938 permissions = ('cloudwatch:PutMetricAlarm',)
1939 valid_origin_states = ('running', 'stopped', 'pending', 'stopping')
1940 filter_asg_membership = ValueFilter({
1941 'key': 'tag:aws:autoscaling:groupName',
1942 'value': 'empty'}).validate()
1944 def process(self, instances):
1945 instances = self.filter_asg_membership.process(
1946 self.filter_resources(instances, 'State.Name', self.valid_origin_states))
1947 if not len(instances):
1948 return
1949 client = utils.local_session(
1950 self.manager.session_factory).client('cloudwatch')
1951 for i in instances:
1952 client.put_metric_alarm(
1953 AlarmName='recover-{}'.format(i['InstanceId']),
1954 AlarmDescription='Auto Recover {}'.format(i['InstanceId']),
1955 ActionsEnabled=True,
1956 AlarmActions=[
1957 'arn:{}:automate:{}:ec2:recover'.format(
1958 utils.REGION_PARTITION_MAP.get(
1959 self.manager.config.region, 'aws'),
1960 i['Placement']['AvailabilityZone'][:-1])
1961 ],
1962 MetricName='StatusCheckFailed_System',
1963 Namespace='AWS/EC2',
1964 Statistic='Minimum',
1965 Dimensions=[
1966 {
1967 'Name': 'InstanceId',
1968 'Value': i['InstanceId']
1969 }
1970 ],
1971 Period=60,
1972 EvaluationPeriods=2,
1973 Threshold=0,
1974 ComparisonOperator='GreaterThanThreshold'
1975 )
1978@actions.register('set-instance-profile')
1979class SetInstanceProfile(BaseAction):
1980 """Sets (add, modify, remove) the instance profile for a running EC2 instance.
1982 :Example:
1984 .. code-block:: yaml
1986 policies:
1987 - name: set-default-instance-profile
1988 resource: ec2
1989 filters:
1990 - IamInstanceProfile: absent
1991 actions:
1992 - type: set-instance-profile
1993 name: default
1995 https://docs.aws.amazon.com/cli/latest/reference/ec2/associate-iam-instance-profile.html
1996 https://docs.aws.amazon.com/cli/latest/reference/ec2/disassociate-iam-instance-profile.html
1997 """
1999 schema = type_schema(
2000 'set-instance-profile',
2001 **{'name': {'type': 'string'}})
2003 permissions = (
2004 'ec2:AssociateIamInstanceProfile',
2005 'ec2:DisassociateIamInstanceProfile',
2006 'iam:PassRole')
2008 valid_origin_states = ('running', 'pending', 'stopped', 'stopping')
2010 def process(self, instances):
2011 instances = self.filter_resources(instances, 'State.Name', self.valid_origin_states)
2012 if not len(instances):
2013 return
2014 client = utils.local_session(self.manager.session_factory).client('ec2')
2015 profile_name = self.data.get('name')
2016 profile_instances = [i for i in instances if i.get('IamInstanceProfile')]
2018 if profile_instances:
2019 associations = {
2020 a['InstanceId']: (a['AssociationId'], a['IamInstanceProfile']['Arn'])
2021 for a in client.describe_iam_instance_profile_associations(
2022 Filters=[
2023 {'Name': 'instance-id',
2024 'Values': [i['InstanceId'] for i in profile_instances]},
2025 {'Name': 'state', 'Values': ['associating', 'associated']}]
2026 ).get('IamInstanceProfileAssociations', ())}
2027 else:
2028 associations = {}
2030 for i in instances:
2031 if profile_name and i['InstanceId'] not in associations:
2032 client.associate_iam_instance_profile(
2033 IamInstanceProfile={'Name': profile_name},
2034 InstanceId=i['InstanceId'])
2035 continue
2036 # Removing profile and no profile on instance.
2037 elif profile_name is None and i['InstanceId'] not in associations:
2038 continue
2040 p_assoc_id, p_arn = associations[i['InstanceId']]
2042 # Already associated to target profile, skip
2043 if profile_name and p_arn.endswith('/%s' % profile_name):
2044 continue
2046 if profile_name is None:
2047 client.disassociate_iam_instance_profile(
2048 AssociationId=p_assoc_id)
2049 else:
2050 client.replace_iam_instance_profile_association(
2051 IamInstanceProfile={'Name': profile_name},
2052 AssociationId=p_assoc_id)
2054 return instances
2057@actions.register('propagate-spot-tags')
2058class PropagateSpotTags(BaseAction):
2059 """Propagate Tags that are set at Spot Request level to EC2 instances.
2061 :Example:
2063 .. code-block:: yaml
2065 policies:
2066 - name: ec2-spot-instances
2067 resource: ec2
2068 filters:
2069 - State.Name: pending
2070 - instanceLifecycle: spot
2071 actions:
2072 - type: propagate-spot-tags
2073 only_tags:
2074 - Name
2075 - BillingTag
2076 """
2078 schema = type_schema(
2079 'propagate-spot-tags',
2080 **{'only_tags': {'type': 'array', 'items': {'type': 'string'}}})
2082 permissions = (
2083 'ec2:DescribeInstances',
2084 'ec2:DescribeSpotInstanceRequests',
2085 'ec2:DescribeTags',
2086 'ec2:CreateTags')
2088 MAX_TAG_COUNT = 50
2090 def process(self, instances):
2091 instances = [
2092 i for i in instances if i['InstanceLifecycle'] == 'spot']
2093 if not len(instances):
2094 self.log.warning(
2095 "action:%s no spot instances found, implicit filter by action" % (
2096 self.__class__.__name__.lower()))
2097 return
2099 client = utils.local_session(
2100 self.manager.session_factory).client('ec2')
2102 request_instance_map = {}
2103 for i in instances:
2104 request_instance_map.setdefault(
2105 i['SpotInstanceRequestId'], []).append(i)
2107 # ... and describe the corresponding spot requests ...
2108 requests = client.describe_spot_instance_requests(
2109 Filters=[{
2110 'Name': 'spot-instance-request-id',
2111 'Values': list(request_instance_map.keys())}]).get(
2112 'SpotInstanceRequests', [])
2114 updated = []
2115 for r in requests:
2116 if not r.get('Tags'):
2117 continue
2118 updated.extend(
2119 self.process_request_instances(
2120 client, r, request_instance_map[r['SpotInstanceRequestId']]))
2121 return updated
2123 def process_request_instances(self, client, request, instances):
2124 # Now we find the tags we can copy : either all, either those
2125 # indicated with 'only_tags' parameter.
2126 copy_keys = self.data.get('only_tags', [])
2127 request_tags = {t['Key']: t['Value'] for t in request['Tags']
2128 if not t['Key'].startswith('aws:')}
2129 if copy_keys:
2130 for k in set(copy_keys).difference(request_tags):
2131 del request_tags[k]
2133 update_instances = []
2134 for i in instances:
2135 instance_tags = {t['Key']: t['Value'] for t in i.get('Tags', [])}
2136 # We may overwrite tags, but if the operation changes no tag,
2137 # we will not proceed.
2138 for k, v in request_tags.items():
2139 if k not in instance_tags or instance_tags[k] != v:
2140 update_instances.append(i['InstanceId'])
2142 if len(set(instance_tags) | set(request_tags)) > self.MAX_TAG_COUNT:
2143 self.log.warning(
2144 "action:%s instance:%s too many tags to copy (> 50)" % (
2145 self.__class__.__name__.lower(),
2146 i['InstanceId']))
2147 continue
2149 for iset in utils.chunks(update_instances, 20):
2150 client.create_tags(
2151 DryRun=self.manager.config.dryrun,
2152 Resources=iset,
2153 Tags=[{'Key': k, 'Value': v} for k, v in request_tags.items()])
2155 self.log.debug(
2156 "action:%s tags updated on instances:%r" % (
2157 self.__class__.__name__.lower(),
2158 update_instances))
2160 return update_instances
2163# Valid EC2 Query Filters
2164# http://docs.aws.amazon.com/AWSEC2/latest/CommandLineReference/ApiReference-cmd-DescribeInstances.html
2165EC2_VALID_FILTERS = {
2166 'architecture': ('i386', 'x86_64'),
2167 'availability-zone': str,
2168 'iam-instance-profile.arn': str,
2169 'image-id': str,
2170 'instance-id': str,
2171 'instance-lifecycle': ('spot',),
2172 'instance-state-name': (
2173 'pending',
2174 'terminated',
2175 'running',
2176 'shutting-down',
2177 'stopping',
2178 'stopped'),
2179 'instance.group-id': str,
2180 'instance.group-name': str,
2181 'tag-key': str,
2182 'tag-value': str,
2183 'tag:': str,
2184 'tenancy': ('dedicated', 'default', 'host'),
2185 'vpc-id': str}
2188class QueryFilter:
2190 @classmethod
2191 def parse(cls, data):
2192 results = []
2193 for d in data:
2194 if not isinstance(d, dict):
2195 raise ValueError(
2196 "EC2 Query Filter Invalid structure %s" % d)
2197 results.append(cls(d).validate())
2198 return results
2200 def __init__(self, data):
2201 self.data = data
2202 self.key = None
2203 self.value = None
2205 def validate(self):
2206 if not len(list(self.data.keys())) == 1:
2207 raise PolicyValidationError(
2208 "EC2 Query Filter Invalid %s" % self.data)
2209 self.key = list(self.data.keys())[0]
2210 self.value = list(self.data.values())[0]
2212 if self.key not in EC2_VALID_FILTERS and not self.key.startswith(
2213 'tag:'):
2214 raise PolicyValidationError(
2215 "EC2 Query Filter invalid filter name %s" % (self.data))
2217 if self.value is None:
2218 raise PolicyValidationError(
2219 "EC2 Query Filters must have a value, use tag-key"
2220 " w/ tag name as value for tag present checks"
2221 " %s" % self.data)
2222 return self
2224 def query(self):
2225 value = self.value
2226 if isinstance(self.value, str):
2227 value = [self.value]
2229 return {'Name': self.key, 'Values': value}
2232@filters.register('instance-attribute')
2233class InstanceAttribute(ValueFilter):
2234 """EC2 Instance Value Filter on a given instance attribute.
2236 Filters EC2 Instances with the given instance attribute
2238 :Example:
2240 .. code-block:: yaml
2242 policies:
2243 - name: ec2-unoptimized-ebs
2244 resource: ec2
2245 filters:
2246 - type: instance-attribute
2247 attribute: ebsOptimized
2248 key: "Value"
2249 value: false
2250 """
2252 valid_attrs = (
2253 'instanceType',
2254 'kernel',
2255 'ramdisk',
2256 'userData',
2257 'disableApiTermination',
2258 'instanceInitiatedShutdownBehavior',
2259 'rootDeviceName',
2260 'blockDeviceMapping',
2261 'productCodes',
2262 'sourceDestCheck',
2263 'groupSet',
2264 'ebsOptimized',
2265 'sriovNetSupport',
2266 'enaSupport')
2268 schema = type_schema(
2269 'instance-attribute',
2270 rinherit=ValueFilter.schema,
2271 attribute={'enum': valid_attrs},
2272 required=('attribute',))
2273 schema_alias = False
2275 def get_permissions(self):
2276 return ('ec2:DescribeInstanceAttribute',)
2278 def process(self, resources, event=None):
2279 attribute = self.data['attribute']
2280 self.get_instance_attribute(resources, attribute)
2281 return [resource for resource in resources
2282 if self.match(resource['c7n:attribute-%s' % attribute])]
2284 def get_instance_attribute(self, resources, attribute):
2285 client = utils.local_session(
2286 self.manager.session_factory).client('ec2')
2288 for resource in resources:
2289 instance_id = resource['InstanceId']
2290 fetched_attribute = self.manager.retry(
2291 client.describe_instance_attribute,
2292 Attribute=attribute,
2293 InstanceId=instance_id)
2294 keys = list(fetched_attribute.keys())
2295 keys.remove('ResponseMetadata')
2296 keys.remove('InstanceId')
2297 resource['c7n:attribute-%s' % attribute] = fetched_attribute[
2298 keys[0]]
2301@resources.register('launch-template-version')
2302class LaunchTemplate(query.QueryResourceManager):
2304 class resource_type(query.TypeInfo):
2305 id = 'LaunchTemplateId'
2306 id_prefix = 'lt-'
2307 name = 'LaunchTemplateName'
2308 service = 'ec2'
2309 date = 'CreateTime'
2310 enum_spec = (
2311 'describe_launch_templates', 'LaunchTemplates', None)
2312 filter_name = 'LaunchTemplateIds'
2313 filter_type = 'list'
2314 arn_type = "launch-template"
2315 cfn_type = "AWS::EC2::LaunchTemplate"
2317 def augment(self, resources):
2318 client = utils.local_session(
2319 self.session_factory).client('ec2')
2320 template_versions = []
2321 for r in resources:
2322 template_versions.extend(
2323 client.describe_launch_template_versions(
2324 LaunchTemplateId=r['LaunchTemplateId']).get(
2325 'LaunchTemplateVersions', ()))
2326 return template_versions
2328 def get_arns(self, resources):
2329 arns = []
2330 for r in resources:
2331 arns.append(self.generate_arn(f"{r['LaunchTemplateId']}/{r['VersionNumber']}"))
2332 return arns
2334 def get_resources(self, rids, cache=True):
2335 # Launch template versions have a compound primary key
2336 #
2337 # Support one of four forms of resource ids:
2338 #
2339 # - array of launch template ids
2340 # - array of tuples (launch template id, version id)
2341 # - array of dicts (with LaunchTemplateId and VersionNumber)
2342 # - array of dicts (with LaunchTemplateId and LatestVersionNumber)
2343 #
2344 # If an alias version is given $Latest, $Default, the alias will be
2345 # preserved as an annotation on the returned object 'c7n:VersionAlias'
2346 if not rids:
2347 return []
2349 t_versions = {}
2350 if isinstance(rids[0], tuple):
2351 for tid, tversion in rids:
2352 t_versions.setdefault(tid, []).append(tversion)
2353 elif isinstance(rids[0], dict):
2354 for tinfo in rids:
2355 t_versions.setdefault(
2356 tinfo['LaunchTemplateId'], []).append(
2357 tinfo.get('VersionNumber', tinfo.get('LatestVersionNumber')))
2358 elif isinstance(rids[0], str):
2359 for tid in rids:
2360 t_versions[tid] = []
2362 client = utils.local_session(self.session_factory).client('ec2')
2364 results = []
2365 # We may end up fetching duplicates on $Latest and $Version
2366 for tid, tversions in t_versions.items():
2367 try:
2368 ltv = client.describe_launch_template_versions(
2369 LaunchTemplateId=tid, Versions=tversions).get(
2370 'LaunchTemplateVersions')
2371 except ClientError as e:
2372 if e.response['Error']['Code'] == "InvalidLaunchTemplateId.NotFound":
2373 continue
2374 if e.response['Error']['Code'] == "InvalidLaunchTemplateId.VersionNotFound":
2375 continue
2376 raise
2377 if not tversions:
2378 tversions = [str(t['VersionNumber']) for t in ltv]
2379 for tversion, t in zip(tversions, ltv):
2380 if not tversion.isdigit():
2381 t['c7n:VersionAlias'] = tversion
2382 results.append(t)
2383 return results
2385 def get_asg_templates(self, asgs):
2386 templates = {}
2387 for a in asgs:
2388 t = None
2389 if 'LaunchTemplate' in a:
2390 t = a['LaunchTemplate']
2391 elif 'MixedInstancesPolicy' in a:
2392 t = a['MixedInstancesPolicy'][
2393 'LaunchTemplate']['LaunchTemplateSpecification']
2394 if t is None:
2395 continue
2396 templates.setdefault(
2397 (t['LaunchTemplateId'],
2398 t.get('Version', '$Default')), []).append(a['AutoScalingGroupName'])
2399 return templates
2402@resources.register('ec2-reserved')
2403class ReservedInstance(query.QueryResourceManager):
2405 class resource_type(query.TypeInfo):
2406 service = 'ec2'
2407 name = id = 'ReservedInstancesId'
2408 id_prefix = ""
2409 date = 'Start'
2410 enum_spec = (
2411 'describe_reserved_instances', 'ReservedInstances', None)
2412 filter_name = 'ReservedInstancesIds'
2413 filter_type = 'list'
2414 arn_type = "reserved-instances"
2417@resources.register('ec2-host')
2418class DedicatedHost(query.QueryResourceManager):
2419 """Custodian resource for managing EC2 Dedicated Hosts.
2420 """
2422 class resource_type(query.TypeInfo):
2423 service = 'ec2'
2424 name = id = 'HostId'
2425 id_prefix = 'h-'
2426 enum_spec = ('describe_hosts', 'Hosts', None)
2427 arn_type = "dedicated-host"
2428 filter_name = 'HostIds'
2429 filter_type = 'list'
2430 date = 'AllocationTime'
2431 cfn_type = config_type = 'AWS::EC2::Host'
2432 permissions_enum = ('ec2:DescribeHosts',)
2435@resources.register('ec2-spot-fleet-request')
2436class SpotFleetRequest(query.QueryResourceManager):
2437 """Custodian resource for managing EC2 Spot Fleet Requests.
2438 """
2440 class resource_type(query.TypeInfo):
2441 service = 'ec2'
2442 name = id = 'SpotFleetRequestId'
2443 id_prefix = 'sfr-'
2444 enum_spec = ('describe_spot_fleet_requests', 'SpotFleetRequestConfigs', None)
2445 filter_name = 'SpotFleetRequestIds'
2446 filter_type = 'list'
2447 date = 'CreateTime'
2448 arn_type = 'spot-fleet-request'
2449 config_type = cfn_type = 'AWS::EC2::SpotFleet'
2450 permissions_enum = ('ec2:DescribeSpotFleetRequests',)
2453SpotFleetRequest.filter_registry.register('offhour', OffHour)
2454SpotFleetRequest.filter_registry.register('onhour', OnHour)
2457@SpotFleetRequest.action_registry.register('resize')
2458class AutoscalingSpotFleetRequest(AutoscalingBase):
2459 permissions = (
2460 'ec2:CreateTags',
2461 'ec2:ModifySpotFleetRequest',
2462 )
2464 service_namespace = 'ec2'
2465 scalable_dimension = 'ec2:spot-fleet-request:TargetCapacity'
2467 def get_resource_id(self, resource):
2468 return 'spot-fleet-request/%s' % resource['SpotFleetRequestId']
2470 def get_resource_tag(self, resource, key):
2471 if 'Tags' in resource:
2472 for tag in resource['Tags']:
2473 if tag['Key'] == key:
2474 return tag['Value']
2475 return None
2477 def get_resource_desired(self, resource):
2478 return int(resource['SpotFleetRequestConfig']['TargetCapacity'])
2480 def set_resource_desired(self, resource, desired):
2481 client = utils.local_session(self.manager.session_factory).client('ec2')
2482 client.modify_spot_fleet_request(
2483 SpotFleetRequestId=resource['SpotFleetRequestId'],
2484 TargetCapacity=desired,
2485 )
2488@EC2.filter_registry.register('has-specific-managed-policy')
2489class HasSpecificManagedPolicy(SpecificIamProfileManagedPolicy):
2490 """Filter an EC2 instance that has an IAM instance profile that contains an IAM role that has
2491 a specific managed IAM policy. If an EC2 instance does not have a profile or the profile
2492 does not contain an IAM role, then it will be treated as not having the policy.
2494 :example:
2496 .. code-block:: yaml
2498 policies:
2499 - name: ec2-instance-has-admin-policy
2500 resource: aws.ec2
2501 filters:
2502 - type: has-specific-managed-policy
2503 value: admin-policy
2505 :example:
2507 Check for EC2 instances with instance profile roles that have an
2508 attached policy matching a given list:
2510 .. code-block:: yaml
2512 policies:
2513 - name: ec2-instance-with-selected-policies
2514 resource: aws.ec2
2515 filters:
2516 - type: has-specific-managed-policy
2517 op: in
2518 value:
2519 - AmazonS3FullAccess
2520 - AWSOrganizationsFullAccess
2522 :example:
2524 Check for EC2 instances with instance profile roles that have
2525 attached policy names matching a pattern:
2527 .. code-block:: yaml
2529 policies:
2530 - name: ec2-instance-with-full-access-policies
2531 resource: aws.ec2
2532 filters:
2533 - type: has-specific-managed-policy
2534 op: glob
2535 value: "*FullAccess"
2537 Check for EC2 instances with instance profile roles that have
2538 attached policy ARNs matching a pattern:
2540 .. code-block:: yaml
2542 policies:
2543 - name: ec2-instance-with-aws-full-access-policies
2544 resource: aws.ec2
2545 filters:
2546 - type: has-specific-managed-policy
2547 key: PolicyArn
2548 op: regex
2549 value: "arn:aws:iam::aws:policy/.*FullAccess"
2550 """
2552 permissions = (
2553 'iam:GetInstanceProfile',
2554 'iam:ListInstanceProfiles',
2555 'iam:ListAttachedRolePolicies')
2557 def process(self, resources, event=None):
2558 client = utils.local_session(self.manager.session_factory).client('iam')
2559 iam_profiles = self.manager.get_resource_manager('iam-profile').resources()
2560 iam_profiles_mapping = {profile['Arn']: profile for profile in iam_profiles}
2562 results = []
2563 for r in resources:
2564 if r['State']['Name'] == 'terminated':
2565 continue
2566 instance_profile_arn = r.get('IamInstanceProfile', {}).get('Arn')
2567 if not instance_profile_arn:
2568 continue
2570 profile = iam_profiles_mapping.get(instance_profile_arn)
2571 if not profile:
2572 continue
2574 self.get_managed_policies(client, [profile])
2576 matched_keys = [k for k in profile[self.annotation_key] if self.match(k)]
2577 self.merge_annotation(profile, self.matched_annotation_key, matched_keys)
2578 if matched_keys:
2579 results.append(r)
2581 return results
2584@resources.register('ec2-capacity-reservation')
2585class CapacityReservation(query.QueryResourceManager):
2586 """Custodian resource for managing EC2 Capacity Reservation.
2587 """
2589 class resource_type(query.TypeInfo):
2590 name = id = 'CapacityReservationId'
2591 service = 'ec2'
2592 enum_spec = ('describe_capacity_reservations',
2593 'CapacityReservations', None)
2595 id_prefix = 'cr-'
2596 arn = "CapacityReservationArn"
2597 filter_name = 'CapacityReservationIds'
2598 filter_type = 'list'
2599 cfn_type = 'AWS::EC2::CapacityReservation'
2600 permissions_enum = ('ec2:DescribeCapacityReservations',)