Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/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, QueryParser
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 query_params = query_params or {}
47 queries = EC2QueryParser.parse(self.manager.data.get('query', []))
48 for q in queries:
49 query_params.update(q)
50 return query_params
52 def augment(self, resources):
53 """EC2 API and AWOL Tags
55 While ec2 api generally returns tags when doing describe_x on for
56 various resources, it may also silently fail to do so unless a tag
57 is used as a filter.
59 See footnote on for official documentation.
60 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#Using_Tags_CLI
62 Apriori we may be using custodian to ensure tags (including
63 name), so there isn't a good default to ensure that we will
64 always get tags from describe_x calls.
65 """
66 # First if we're in event based lambda go ahead and skip this,
67 # tags can't be trusted in ec2 instances immediately post creation.
68 if not resources or self.manager.data.get(
69 'mode', {}).get('type', '') in (
70 'cloudtrail', 'ec2-instance-state'):
71 return resources
73 # AWOL detector, so we don't make extraneous api calls.
74 resource_count = len(resources)
75 search_count = min(int(resource_count % 0.05) + 1, 5)
76 if search_count > resource_count:
77 search_count = resource_count
78 found = False
79 for r in random.sample(resources, search_count):
80 if 'Tags' in r:
81 found = True
82 break
84 if found:
85 return resources
87 # Okay go and do the tag lookup
88 client = utils.local_session(self.manager.session_factory).client('ec2')
89 tag_set = self.manager.retry(
90 client.describe_tags,
91 Filters=[{'Name': 'resource-type',
92 'Values': ['instance']}])['Tags']
93 resource_tags = {}
94 for t in tag_set:
95 t.pop('ResourceType')
96 rid = t.pop('ResourceId')
97 resource_tags.setdefault(rid, []).append(t)
99 m = self.manager.get_model()
100 for r in resources:
101 r['Tags'] = resource_tags.get(r[m.id], [])
102 return resources
105@resources.register('ec2')
106class EC2(query.QueryResourceManager):
108 class resource_type(query.TypeInfo):
109 service = 'ec2'
110 arn_type = 'instance'
111 enum_spec = ('describe_instances', 'Reservations[].Instances[]', None)
112 id = 'InstanceId'
113 filter_name = 'InstanceIds'
114 filter_type = 'list'
115 name = 'PublicDnsName'
116 date = 'LaunchTime'
117 dimension = 'InstanceId'
118 cfn_type = config_type = "AWS::EC2::Instance"
119 id_prefix = 'i-'
120 permissions_augment = ('ec2:DescribeTags',)
122 default_report_fields = (
123 'CustodianDate',
124 'InstanceId',
125 'tag:Name',
126 'InstanceType',
127 'LaunchTime',
128 'VpcId',
129 'PrivateIpAddress',
130 )
132 filter_registry = filters
133 action_registry = actions
135 # if we have to do a fallback scenario where tags don't come in describe
136 permissions = ('ec2:DescribeTags',)
137 source_mapping = {
138 'describe': DescribeEC2,
139 'config': query.ConfigSource
140 }
143@filters.register('security-group')
144class SecurityGroupFilter(net_filters.SecurityGroupFilter):
146 RelatedIdsExpression = "NetworkInterfaces[].Groups[].GroupId"
149@filters.register('subnet')
150class SubnetFilter(net_filters.SubnetFilter):
152 RelatedIdsExpression = "NetworkInterfaces[].SubnetId"
155@filters.register('vpc')
156class VpcFilter(net_filters.VpcFilter):
158 RelatedIdsExpression = "VpcId"
161@filters.register('check-permissions')
162class ComputePermissions(CheckPermissions):
164 def get_iam_arns(self, resources):
165 profile_arn_map = {
166 r['IamInstanceProfile']['Arn']: r['IamInstanceProfile']['Id']
167 for r in resources if 'IamInstanceProfile' in r}
169 # py2 compat on dict ordering
170 profile_arns = list(profile_arn_map.items())
171 profile_role_map = {
172 arn: profile['Roles'][0]['Arn']
173 for arn, profile in zip(
174 [p[0] for p in profile_arns],
175 self.manager.get_resource_manager(
176 'iam-profile').get_resources(
177 [p[0].split('/', 1)[-1] for p in profile_arns]))}
178 return [
179 profile_role_map.get(r.get('IamInstanceProfile', {}).get('Arn'))
180 for r in resources]
183@filters.register('state-age')
184class StateTransitionAge(AgeFilter):
185 """Age an instance has been in the given state.
187 .. code-block:: yaml
189 policies:
190 - name: ec2-state-running-7-days
191 resource: ec2
192 filters:
193 - type: state-age
194 op: ge
195 days: 7
196 """
197 RE_PARSE_AGE = re.compile(r"\(.*?\)")
199 # this filter doesn't use date_attribute, but needs to define it
200 # to pass AgeFilter's validate method
201 date_attribute = "dummy"
203 schema = type_schema(
204 'state-age',
205 op={'$ref': '#/definitions/filters_common/comparison_operators'},
206 days={'type': 'number'})
208 def get_resource_date(self, i):
209 v = i.get('StateTransitionReason')
210 if not v:
211 return None
212 dates = self.RE_PARSE_AGE.findall(v)
213 if dates:
214 return parse(dates[0][1:-1])
215 return None
218@filters.register('ebs')
219class AttachedVolume(ValueFilter):
220 """EC2 instances with EBS backed volume
222 Filters EC2 instances with EBS backed storage devices (non ephemeral)
224 :Example:
226 .. code-block:: yaml
228 policies:
229 - name: ec2-encrypted-ebs-volumes
230 resource: ec2
231 filters:
232 - type: ebs
233 key: Encrypted
234 value: true
235 """
237 schema = type_schema(
238 'ebs', rinherit=ValueFilter.schema,
239 **{'operator': {'enum': ['and', 'or']},
240 'skip-devices': {'type': 'array', 'items': {'type': 'string'}}})
241 schema_alias = False
243 def get_permissions(self):
244 return self.manager.get_resource_manager('ebs').get_permissions()
246 def process(self, resources, event=None):
247 self.volume_map = self.get_volume_mapping(resources)
248 self.skip = self.data.get('skip-devices', [])
249 self.operator = self.data.get(
250 'operator', 'or') == 'or' and any or all
251 return list(filter(self, resources))
253 def get_volume_mapping(self, resources):
254 volume_map = {}
255 manager = self.manager.get_resource_manager('ebs')
256 for instance_set in utils.chunks(resources, 200):
257 volume_ids = []
258 for i in instance_set:
259 for bd in i.get('BlockDeviceMappings', ()):
260 if 'Ebs' not in bd:
261 continue
262 volume_ids.append(bd['Ebs']['VolumeId'])
263 for v in manager.get_resources(volume_ids):
264 if not v['Attachments']:
265 continue
266 volume_map.setdefault(
267 v['Attachments'][0]['InstanceId'], []).append(v)
268 return volume_map
270 def __call__(self, i):
271 volumes = self.volume_map.get(i['InstanceId'])
272 if not volumes:
273 return False
274 if self.skip:
275 for v in list(volumes):
276 for a in v.get('Attachments', []):
277 if a['Device'] in self.skip:
278 volumes.remove(v)
279 return self.operator(map(self.match, volumes))
282@filters.register('stop-protected')
283class DisableApiStop(Filter):
284 """EC2 instances with ``disableApiStop`` attribute set
286 Filters EC2 instances with ``disableApiStop`` attribute set to true.
288 :Example:
290 .. code-block:: yaml
292 policies:
293 - name: stop-protection-enabled
294 resource: ec2
295 filters:
296 - type: stop-protected
298 :Example:
300 .. code-block:: yaml
302 policies:
303 - name: stop-protection-NOT-enabled
304 resource: ec2
305 filters:
306 - not:
307 - type: stop-protected
308 """
310 schema = type_schema('stop-protected')
311 permissions = ('ec2:DescribeInstanceAttribute',)
313 def process(self, resources: List[dict], event=None) -> List[dict]:
314 client = utils.local_session(
315 self.manager.session_factory).client('ec2')
316 return [r for r in resources
317 if self._is_stop_protection_enabled(client, r)]
319 def _is_stop_protection_enabled(self, client, instance: dict) -> bool:
320 attr_val = self.manager.retry(
321 client.describe_instance_attribute,
322 Attribute='disableApiStop',
323 InstanceId=instance['InstanceId']
324 )
325 return attr_val['DisableApiStop']['Value']
327 def validate(self) -> None:
328 botocore_min_version = '1.26.7'
330 if LooseVersion(botocore.__version__) < LooseVersion(botocore_min_version):
331 raise PolicyValidationError(
332 "'stop-protected' filter requires botocore version "
333 f'{botocore_min_version} or above. '
334 f'Installed version is {botocore.__version__}.'
335 )
338@filters.register('termination-protected')
339class DisableApiTermination(Filter):
340 """EC2 instances with ``disableApiTermination`` attribute set
342 Filters EC2 instances with ``disableApiTermination`` attribute set to true.
344 :Example:
346 .. code-block:: yaml
348 policies:
349 - name: termination-protection-enabled
350 resource: ec2
351 filters:
352 - type: termination-protected
354 :Example:
356 .. code-block:: yaml
358 policies:
359 - name: termination-protection-NOT-enabled
360 resource: ec2
361 filters:
362 - not:
363 - type: termination-protected
364 """
366 schema = type_schema('termination-protected')
367 permissions = ('ec2:DescribeInstanceAttribute',)
369 def get_permissions(self):
370 perms = list(self.permissions)
371 perms.extend(self.manager.get_permissions())
372 return perms
374 def process(self, resources, event=None):
375 client = utils.local_session(
376 self.manager.session_factory).client('ec2')
377 return [r for r in resources
378 if self.is_termination_protection_enabled(client, r)]
380 def is_termination_protection_enabled(self, client, inst):
381 attr_val = self.manager.retry(
382 client.describe_instance_attribute,
383 Attribute='disableApiTermination',
384 InstanceId=inst['InstanceId']
385 )
386 return attr_val['DisableApiTermination']['Value']
389class InstanceImageBase:
391 def prefetch_instance_images(self, instances):
392 image_ids = [i['ImageId'] for i in instances if 'c7n:instance-image' not in i]
393 self.image_map = self.get_local_image_mapping(image_ids)
395 def get_base_image_mapping(self):
396 return {i['ImageId']: i for i in
397 self.manager.get_resource_manager('ami').resources()}
399 def get_instance_image(self, instance):
400 image = instance.get('c7n:instance-image', None)
401 if not image:
402 image = instance['c7n:instance-image'] = self.image_map.get(instance['ImageId'], None)
403 return image
405 def get_local_image_mapping(self, image_ids):
406 base_image_map = self.get_base_image_mapping()
407 resources = {i: base_image_map[i] for i in image_ids if i in base_image_map}
408 missing = list(set(image_ids) - set(resources.keys()))
409 if missing:
410 loaded = self.manager.get_resource_manager('ami').get_resources(missing, False)
411 resources.update({image['ImageId']: image for image in loaded})
412 return resources
415@filters.register('image-age')
416class ImageAge(AgeFilter, InstanceImageBase):
417 """EC2 AMI age filter
419 Filters EC2 instances based on the age of their AMI image (in days)
421 :Example:
423 .. code-block:: yaml
425 policies:
426 - name: ec2-ancient-ami
427 resource: ec2
428 filters:
429 - type: image-age
430 op: ge
431 days: 90
432 """
434 date_attribute = "CreationDate"
436 schema = type_schema(
437 'image-age',
438 op={'$ref': '#/definitions/filters_common/comparison_operators'},
439 days={'type': 'number'})
441 def get_permissions(self):
442 return self.manager.get_resource_manager('ami').get_permissions()
444 def process(self, resources, event=None):
445 self.prefetch_instance_images(resources)
446 return super(ImageAge, self).process(resources, event)
448 def get_resource_date(self, i):
449 image = self.get_instance_image(i)
450 if image:
451 return parse(image['CreationDate'])
452 else:
453 return parse("2000-01-01T01:01:01.000Z")
456@filters.register('image')
457class InstanceImage(ValueFilter, InstanceImageBase):
459 schema = type_schema('image', rinherit=ValueFilter.schema)
460 schema_alias = False
462 def get_permissions(self):
463 return self.manager.get_resource_manager('ami').get_permissions()
465 def process(self, resources, event=None):
466 self.prefetch_instance_images(resources)
467 return super(InstanceImage, self).process(resources, event)
469 def __call__(self, i):
470 image = self.get_instance_image(i)
471 # Finally, if we have no image...
472 if not image:
473 self.log.warning(
474 "Could not locate image for instance:%s ami:%s" % (
475 i['InstanceId'], i["ImageId"]))
476 # Match instead on empty skeleton?
477 return False
478 return self.match(image)
481@filters.register('offhour')
482class InstanceOffHour(OffHour):
483 """Custodian OffHour filter
485 Filters running EC2 instances with the intent to stop at a given hour of
486 the day. A list of days to excluded can be included as a list of strings
487 with the format YYYY-MM-DD. Alternatively, the list (using the same syntax)
488 can be taken from a specified url.
490 Note: You can disable filtering of only running instances by setting
491 `state-filter: false`
493 :Example:
495 .. code-block:: yaml
497 policies:
498 - name: offhour-evening-stop
499 resource: ec2
500 filters:
501 - type: offhour
502 tag: custodian_downtime
503 default_tz: et
504 offhour: 20
505 actions:
506 - stop
508 - name: offhour-evening-stop-skip-holidays
509 resource: ec2
510 filters:
511 - type: offhour
512 tag: custodian_downtime
513 default_tz: et
514 offhour: 20
515 skip-days: ['2017-12-25']
516 actions:
517 - stop
519 - name: offhour-evening-stop-skip-holidays-from
520 resource: ec2
521 filters:
522 - type: offhour
523 tag: custodian_downtime
524 default_tz: et
525 offhour: 20
526 skip-days-from:
527 expr: 0
528 format: csv
529 url: 's3://location/holidays.csv'
530 actions:
531 - stop
532 """
534 schema = type_schema(
535 'offhour', rinherit=OffHour.schema,
536 **{'state-filter': {'type': 'boolean'}})
537 schema_alias = False
539 valid_origin_states = ('running',)
541 def process(self, resources, event=None):
542 if self.data.get('state-filter', True):
543 return super(InstanceOffHour, self).process(
544 self.filter_resources(resources, 'State.Name', self.valid_origin_states))
545 else:
546 return super(InstanceOffHour, self).process(resources)
549@filters.register('network-location')
550class EC2NetworkLocation(net_filters.NetworkLocation):
552 valid_origin_states = ('pending', 'running', 'shutting-down', 'stopping',
553 'stopped')
555 def process(self, resources, event=None):
556 resources = self.filter_resources(resources, 'State.Name', self.valid_origin_states)
557 if not resources:
558 return []
559 return super(EC2NetworkLocation, self).process(resources)
562@filters.register('onhour')
563class InstanceOnHour(OnHour):
564 """Custodian OnHour filter
566 Filters stopped EC2 instances with the intent to start at a given hour of
567 the day. A list of days to excluded can be included as a list of strings
568 with the format YYYY-MM-DD. Alternatively, the list (using the same syntax)
569 can be taken from a specified url.
571 Note: You can disable filtering of only stopped instances by setting
572 `state-filter: false`
574 :Example:
576 .. code-block:: yaml
578 policies:
579 - name: onhour-morning-start
580 resource: ec2
581 filters:
582 - type: onhour
583 tag: custodian_downtime
584 default_tz: et
585 onhour: 6
586 actions:
587 - start
589 - name: onhour-morning-start-skip-holidays
590 resource: ec2
591 filters:
592 - type: onhour
593 tag: custodian_downtime
594 default_tz: et
595 onhour: 6
596 skip-days: ['2017-12-25']
597 actions:
598 - start
600 - name: onhour-morning-start-skip-holidays-from
601 resource: ec2
602 filters:
603 - type: onhour
604 tag: custodian_downtime
605 default_tz: et
606 onhour: 6
607 skip-days-from:
608 expr: 0
609 format: csv
610 url: 's3://location/holidays.csv'
611 actions:
612 - start
613 """
615 schema = type_schema(
616 'onhour', rinherit=OnHour.schema,
617 **{'state-filter': {'type': 'boolean'}})
618 schema_alias = False
620 valid_origin_states = ('stopped',)
622 def process(self, resources, event=None):
623 if self.data.get('state-filter', True):
624 return super(InstanceOnHour, self).process(
625 self.filter_resources(resources, 'State.Name', self.valid_origin_states))
626 else:
627 return super(InstanceOnHour, self).process(resources)
630@filters.register('ephemeral')
631class EphemeralInstanceFilter(Filter):
632 """EC2 instances with ephemeral storage
634 Filters EC2 instances that have ephemeral storage (an instance-store backed
635 root device)
637 :Example:
639 .. code-block:: yaml
641 policies:
642 - name: ec2-ephemeral-instances
643 resource: ec2
644 filters:
645 - type: ephemeral
647 http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html
648 """
650 schema = type_schema('ephemeral')
652 def __call__(self, i):
653 return self.is_ephemeral(i)
655 @staticmethod
656 def is_ephemeral(i):
657 for bd in i.get('BlockDeviceMappings', []):
658 if bd['DeviceName'] in ('/dev/sda1', '/dev/xvda', 'xvda'):
659 if 'Ebs' in bd:
660 return False
661 return True
662 return True
665@filters.register('instance-uptime')
666class UpTimeFilter(AgeFilter):
668 date_attribute = "LaunchTime"
670 schema = type_schema(
671 'instance-uptime',
672 op={'$ref': '#/definitions/filters_common/comparison_operators'},
673 days={'type': 'number'})
676@filters.register('instance-age')
677class InstanceAgeFilter(AgeFilter):
678 """Filters instances based on their age (in days)
680 :Example:
682 .. code-block:: yaml
684 policies:
685 - name: ec2-30-days-plus
686 resource: ec2
687 filters:
688 - type: instance-age
689 op: ge
690 days: 30
691 """
693 date_attribute = "LaunchTime"
694 ebs_key_func = operator.itemgetter('AttachTime')
696 schema = type_schema(
697 'instance-age',
698 op={'$ref': '#/definitions/filters_common/comparison_operators'},
699 days={'type': 'number'},
700 hours={'type': 'number'},
701 minutes={'type': 'number'})
703 def get_resource_date(self, i):
704 # LaunchTime is basically how long has the instance
705 # been on, use the oldest ebs vol attach time
706 ebs_vols = [
707 block['Ebs'] for block in i['BlockDeviceMappings']
708 if 'Ebs' in block]
709 if not ebs_vols:
710 # Fall back to using age attribute (ephemeral instances)
711 return super(InstanceAgeFilter, self).get_resource_date(i)
712 # Lexographical sort on date
713 ebs_vols = sorted(ebs_vols, key=self.ebs_key_func)
714 return ebs_vols[0]['AttachTime']
717@filters.register('default-vpc')
718class DefaultVpc(net_filters.DefaultVpcBase):
719 """ Matches if an ec2 database is in the default vpc
720 """
722 schema = type_schema('default-vpc')
724 def __call__(self, ec2):
725 return ec2.get('VpcId') and self.match(ec2.get('VpcId')) or False
728def deserialize_user_data(user_data):
729 data = base64.b64decode(user_data)
730 # try raw and compressed
731 try:
732 return data.decode('utf8')
733 except UnicodeDecodeError:
734 return zlib.decompress(data, 16).decode('utf8')
737@filters.register('user-data')
738class UserData(ValueFilter):
739 """Filter on EC2 instances which have matching userdata.
740 Note: It is highly recommended to use regexes with the ?sm flags, since Custodian
741 uses re.match() and userdata spans multiple lines.
743 :example:
745 .. code-block:: yaml
747 policies:
748 - name: ec2_userdata_stop
749 resource: ec2
750 filters:
751 - type: user-data
752 op: regex
753 value: (?smi).*password=
754 actions:
755 - stop
756 """
758 schema = type_schema('user-data', rinherit=ValueFilter.schema)
759 schema_alias = False
760 batch_size = 50
761 annotation = 'c7n:user-data'
762 permissions = ('ec2:DescribeInstanceAttribute',)
764 def __init__(self, data, manager):
765 super(UserData, self).__init__(data, manager)
766 self.data['key'] = '"c7n:user-data"'
768 def process(self, resources, event=None):
769 client = utils.local_session(self.manager.session_factory).client('ec2')
770 results = []
771 with self.executor_factory(max_workers=3) as w:
772 futures = {}
773 for instance_set in utils.chunks(resources, self.batch_size):
774 futures[w.submit(
775 self.process_instance_set,
776 client, instance_set)] = instance_set
778 for f in as_completed(futures):
779 if f.exception():
780 self.log.error(
781 "Error processing userdata on instance set %s", f.exception())
782 results.extend(f.result())
783 return results
785 def process_instance_set(self, client, resources):
786 results = []
787 for r in resources:
788 if self.annotation not in r:
789 try:
790 result = client.describe_instance_attribute(
791 Attribute='userData',
792 InstanceId=r['InstanceId'])
793 except ClientError as e:
794 if e.response['Error']['Code'] == 'InvalidInstanceId.NotFound':
795 continue
796 if 'Value' not in result['UserData']:
797 r[self.annotation] = None
798 else:
799 r[self.annotation] = deserialize_user_data(
800 result['UserData']['Value'])
801 if self.match(r):
802 results.append(r)
803 return results
806@filters.register('singleton')
807class SingletonFilter(Filter):
808 """EC2 instances without autoscaling or a recover alarm
810 Filters EC2 instances that are not members of an autoscaling group
811 and do not have Cloudwatch recover alarms.
813 :Example:
815 .. code-block:: yaml
817 policies:
818 - name: ec2-recover-instances
819 resource: ec2
820 filters:
821 - singleton
822 actions:
823 - type: tag
824 key: problem
825 value: instance is not resilient
827 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-recover.html
828 """
830 schema = type_schema('singleton')
832 permissions = ('cloudwatch:DescribeAlarmsForMetric',)
834 valid_origin_states = ('running', 'stopped', 'pending', 'stopping')
836 in_asg = ValueFilter({
837 'key': 'tag:aws:autoscaling:groupName',
838 'value': 'not-null'}).validate()
840 def process(self, instances, event=None):
841 return super(SingletonFilter, self).process(
842 self.filter_resources(instances, 'State.Name', self.valid_origin_states))
844 def __call__(self, i):
845 if self.in_asg(i):
846 return False
847 else:
848 return not self.has_recover_alarm(i)
850 def has_recover_alarm(self, i):
851 client = utils.local_session(self.manager.session_factory).client('cloudwatch')
852 alarms = client.describe_alarms_for_metric(
853 MetricName='StatusCheckFailed_System',
854 Namespace='AWS/EC2',
855 Dimensions=[
856 {
857 'Name': 'InstanceId',
858 'Value': i['InstanceId']
859 }
860 ]
861 )
863 for i in alarms['MetricAlarms']:
864 for a in i['AlarmActions']:
865 if (
866 a.startswith('arn:aws:automate:') and
867 a.endswith(':ec2:recover')
868 ):
869 return True
871 return False
874@EC2.filter_registry.register('ssm')
875class SsmStatus(ValueFilter):
876 """Filter ec2 instances by their ssm status information.
878 :Example:
880 Find ubuntu 18.04 instances are active with ssm.
882 .. code-block:: yaml
884 policies:
885 - name: ec2-ssm-check
886 resource: ec2
887 filters:
888 - type: ssm
889 key: PingStatus
890 value: Online
891 - type: ssm
892 key: PlatformName
893 value: Ubuntu
894 - type: ssm
895 key: PlatformVersion
896 value: 18.04
897 """
898 schema = type_schema('ssm', rinherit=ValueFilter.schema)
899 schema_alias = False
900 permissions = ('ssm:DescribeInstanceInformation',)
901 annotation = 'c7n:SsmState'
903 def process(self, resources, event=None):
904 client = utils.local_session(self.manager.session_factory).client('ssm')
905 results = []
906 for resource_set in utils.chunks(
907 [r for r in resources if self.annotation not in r], 50):
908 self.process_resource_set(client, resource_set)
909 for r in resources:
910 if self.match(r[self.annotation]):
911 results.append(r)
912 return results
914 def process_resource_set(self, client, resources):
915 instance_ids = [i['InstanceId'] for i in resources]
916 info_map = {
917 info['InstanceId']: info for info in
918 client.describe_instance_information(
919 Filters=[{'Key': 'InstanceIds', 'Values': instance_ids}]).get(
920 'InstanceInformationList', [])}
921 for r in resources:
922 r[self.annotation] = info_map.get(r['InstanceId'], {})
925@EC2.filter_registry.register('ssm-inventory')
926class SsmInventory(Filter):
927 """Filter EC2 instances by their SSM software inventory.
929 :Example:
931 Find instances that have a specific package installed.
933 .. code-block:: yaml
935 policies:
936 - name: ec2-find-specific-package
937 resource: ec2
938 filters:
939 - type: ssm-inventory
940 query:
941 - Key: Name
942 Values:
943 - "docker"
944 Type: Equal
946 - name: ec2-get-all-packages
947 resource: ec2
948 filters:
949 - type: ssm-inventory
950 """
951 schema = type_schema(
952 'ssm-inventory',
953 **{'query': {'type': 'array', 'items': {
954 'type': 'object',
955 'properties': {
956 'Key': {'type': 'string'},
957 'Values': {'type': 'array', 'items': {'type': 'string'}},
958 'Type': {'enum': ['Equal', 'NotEqual', 'BeginWith', 'LessThan',
959 'GreaterThan', 'Exists']}},
960 'required': ['Key', 'Values']}}})
962 permissions = ('ssm:ListInventoryEntries',)
963 annotation_key = 'c7n:SSM-Inventory'
965 def process(self, resources, event=None):
966 client = utils.local_session(self.manager.session_factory).client('ssm')
967 query = self.data.get("query")
968 found = []
969 for r in resources:
970 entries = []
971 next_token = None
972 while True:
973 params = {
974 "InstanceId": r["InstanceId"],
975 "TypeName": "AWS:Application"
976 }
977 if next_token:
978 params['NextToken'] = next_token
979 if query:
980 params['Filters'] = query
981 response = client.list_inventory_entries(**params)
982 all_entries = response["Entries"]
983 if all_entries:
984 entries.extend(all_entries)
985 next_token = response.get('NextToken')
986 if not next_token:
987 break
988 if entries:
989 r[self.annotation_key] = entries
990 found.append(r)
991 return found
994@EC2.filter_registry.register('ssm-compliance')
995class SsmCompliance(Filter):
996 """Filter ec2 instances by their ssm compliance status.
998 :Example:
1000 Find non-compliant ec2 instances.
1002 .. code-block:: yaml
1004 policies:
1005 - name: ec2-ssm-compliance
1006 resource: ec2
1007 filters:
1008 - type: ssm-compliance
1009 compliance_types:
1010 - Association
1011 - Patch
1012 severity:
1013 - CRITICAL
1014 - HIGH
1015 - MEDIUM
1016 - LOW
1017 - UNSPECIFIED
1018 states:
1019 - NON_COMPLIANT
1020 eval_filters:
1021 - type: value
1022 key: ExecutionSummary.ExecutionTime
1023 value_type: age
1024 value: 30
1025 op: less-than
1026 """
1027 schema = type_schema(
1028 'ssm-compliance',
1029 **{'required': ['compliance_types'],
1030 'compliance_types': {'type': 'array', 'items': {'type': 'string'}},
1031 'severity': {'type': 'array', 'items': {'type': 'string'}},
1032 'op': {'enum': ['or', 'and']},
1033 'eval_filters': {'type': 'array', 'items': {
1034 'oneOf': [
1035 {'$ref': '#/definitions/filters/valuekv'},
1036 {'$ref': '#/definitions/filters/value'}]}},
1037 'states': {'type': 'array',
1038 'default': ['NON_COMPLIANT'],
1039 'items': {
1040 'enum': [
1041 'COMPLIANT',
1042 'NON_COMPLIANT'
1043 ]}}})
1044 permissions = ('ssm:ListResourceComplianceSummaries',)
1045 annotation = 'c7n:ssm-compliance'
1047 def process(self, resources, event=None):
1048 op = self.data.get('op', 'or') == 'or' and any or all
1049 eval_filters = []
1050 for f in self.data.get('eval_filters', ()):
1051 vf = ValueFilter(f)
1052 vf.annotate = False
1053 eval_filters.append(vf)
1055 client = utils.local_session(self.manager.session_factory).client('ssm')
1056 filters = [
1057 {
1058 'Key': 'Status',
1059 'Values': self.data['states'],
1060 'Type': 'EQUAL'
1061 },
1062 {
1063 'Key': 'ComplianceType',
1064 'Values': self.data['compliance_types'],
1065 'Type': 'EQUAL'
1066 }
1067 ]
1068 severity = self.data.get('severity')
1069 if severity:
1070 filters.append(
1071 {
1072 'Key': 'OverallSeverity',
1073 'Values': severity,
1074 'Type': 'EQUAL'
1075 })
1077 resource_map = {}
1078 pager = client.get_paginator('list_resource_compliance_summaries')
1079 for page in pager.paginate(Filters=filters):
1080 items = page['ResourceComplianceSummaryItems']
1081 for i in items:
1082 if not eval_filters:
1083 resource_map.setdefault(
1084 i['ResourceId'], []).append(i)
1085 continue
1086 if op([f.match(i) for f in eval_filters]):
1087 resource_map.setdefault(
1088 i['ResourceId'], []).append(i)
1090 results = []
1091 for r in resources:
1092 result = resource_map.get(r['InstanceId'])
1093 if result:
1094 r[self.annotation] = result
1095 results.append(r)
1097 return results
1100@actions.register('set-monitoring')
1101class MonitorInstances(BaseAction):
1102 """Action on EC2 Instances to enable/disable detailed monitoring
1104 The different states of detailed monitoring status are :
1105 'disabled'|'disabling'|'enabled'|'pending'
1106 (https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.describe_instances)
1108 :Example:
1110 .. code-block:: yaml
1112 policies:
1113 - name: ec2-detailed-monitoring-activation
1114 resource: ec2
1115 filters:
1116 - Monitoring.State: disabled
1117 actions:
1118 - type: set-monitoring
1119 state: enable
1121 References
1123 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-cloudwatch-new.html
1124 """
1125 schema = type_schema('set-monitoring',
1126 **{'state': {'enum': ['enable', 'disable']}})
1127 permissions = ('ec2:MonitorInstances', 'ec2:UnmonitorInstances')
1129 def process(self, resources, event=None):
1130 client = utils.local_session(
1131 self.manager.session_factory).client('ec2')
1132 actions = {
1133 'enable': self.enable_monitoring,
1134 'disable': self.disable_monitoring
1135 }
1136 for instances_set in utils.chunks(resources, 20):
1137 actions[self.data.get('state')](client, instances_set)
1139 def enable_monitoring(self, client, resources):
1140 try:
1141 client.monitor_instances(
1142 InstanceIds=[inst['InstanceId'] for inst in resources]
1143 )
1144 except ClientError as e:
1145 if e.response['Error']['Code'] != 'InvalidInstanceId.NotFound':
1146 raise
1148 def disable_monitoring(self, client, resources):
1149 try:
1150 client.unmonitor_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
1158@EC2.action_registry.register('set-metadata-access')
1159class SetMetadataServerAccess(BaseAction):
1160 """Set instance metadata server access for an instance.
1162 :example:
1164 Require instances to use IMDSv2
1166 .. code-block:: yaml
1168 policies:
1169 - name: ec2-require-imdsv2
1170 resource: ec2
1171 filters:
1172 - MetadataOptions.HttpTokens: optional
1173 actions:
1174 - type: set-metadata-access
1175 tokens: required
1177 :example:
1179 Disable metadata server access
1181 .. code-block: yaml
1183 policies:
1184 - name: ec2-disable-imds
1185 resource: ec2
1186 filters:
1187 - MetadataOptions.HttpEndpoint: enabled
1188 actions:
1189 - type: set-metadata-access
1190 endpoint: disabled
1192 policies:
1193 - name: ec2-enable-metadata-tags
1194 resource: ec2
1195 filters:
1196 - MetadataOptions.InstanceMetadataTags: disabled
1197 actions:
1198 - type: set-metadata-access
1199 metadata-tags: enabled
1201 Reference: https://amzn.to/2XOuxpQ
1202 """
1204 AllowedValues = {
1205 'HttpEndpoint': ['enabled', 'disabled'],
1206 'HttpTokens': ['required', 'optional'],
1207 'InstanceMetadataTags': ['enabled', 'disabled'],
1208 'HttpPutResponseHopLimit': list(range(1, 65))
1209 }
1211 schema = type_schema(
1212 'set-metadata-access',
1213 anyOf=[{'required': ['endpoint']},
1214 {'required': ['tokens']},
1215 {'required': ['metadatatags']},
1216 {'required': ['hop-limit']}],
1217 **{'endpoint': {'enum': AllowedValues['HttpEndpoint']},
1218 'tokens': {'enum': AllowedValues['HttpTokens']},
1219 'metadata-tags': {'enum': AllowedValues['InstanceMetadataTags']},
1220 'hop-limit': {'type': 'integer', 'minimum': 1, 'maximum': 64}}
1221 )
1222 permissions = ('ec2:ModifyInstanceMetadataOptions',)
1224 def get_params(self):
1225 return filter_empty({
1226 'HttpEndpoint': self.data.get('endpoint'),
1227 'HttpTokens': self.data.get('tokens'),
1228 'InstanceMetadataTags': self.data.get('metadata-tags'),
1229 'HttpPutResponseHopLimit': self.data.get('hop-limit')})
1231 def process(self, resources):
1232 params = self.get_params()
1233 for k, v in params.items():
1234 allowed_values = list(self.AllowedValues[k])
1235 allowed_values.remove(v)
1236 resources = self.filter_resources(
1237 resources, 'MetadataOptions.%s' % k, allowed_values)
1239 if not resources:
1240 return
1242 client = utils.local_session(self.manager.session_factory).client('ec2')
1243 for r in resources:
1244 self.manager.retry(
1245 client.modify_instance_metadata_options,
1246 ignore_err_codes=('InvalidInstanceId.NotFound',),
1247 InstanceId=r['InstanceId'],
1248 **params)
1251@EC2.action_registry.register("post-finding")
1252class InstanceFinding(PostFinding):
1254 resource_type = 'AwsEc2Instance'
1256 def format_resource(self, r):
1257 ip_addresses = jmespath_search(
1258 "NetworkInterfaces[].PrivateIpAddresses[].PrivateIpAddress", r)
1260 # limit to max 10 ip addresses, per security hub service limits
1261 ip_addresses = ip_addresses and ip_addresses[:10] or ip_addresses
1262 details = {
1263 "Type": r["InstanceType"],
1264 "ImageId": r["ImageId"],
1265 "IpV4Addresses": ip_addresses,
1266 "KeyName": r.get("KeyName"),
1267 "LaunchedAt": r["LaunchTime"].isoformat()
1268 }
1270 if "VpcId" in r:
1271 details["VpcId"] = r["VpcId"]
1272 if "SubnetId" in r:
1273 details["SubnetId"] = r["SubnetId"]
1274 # config will use an empty key
1275 if "IamInstanceProfile" in r and r['IamInstanceProfile']:
1276 details["IamInstanceProfileArn"] = r["IamInstanceProfile"]["Arn"]
1278 instance = {
1279 "Type": self.resource_type,
1280 "Id": "arn:{}:ec2:{}:{}:instance/{}".format(
1281 utils.REGION_PARTITION_MAP.get(self.manager.config.region, 'aws'),
1282 self.manager.config.region,
1283 self.manager.config.account_id,
1284 r["InstanceId"]),
1285 "Region": self.manager.config.region,
1286 "Tags": {t["Key"]: t["Value"] for t in r.get("Tags", [])},
1287 "Details": {self.resource_type: filter_empty(details)},
1288 }
1290 instance = filter_empty(instance)
1291 return instance
1294@actions.register('start')
1295class Start(BaseAction):
1296 """Starts a previously stopped EC2 instance.
1298 :Example:
1300 .. code-block:: yaml
1302 policies:
1303 - name: ec2-start-stopped-instances
1304 resource: ec2
1305 query:
1306 - instance-state-name: stopped
1307 actions:
1308 - start
1310 http://docs.aws.amazon.com/cli/latest/reference/ec2/start-instances.html
1311 """
1313 valid_origin_states = ('stopped',)
1314 schema = type_schema('start')
1315 permissions = ('ec2:StartInstances',)
1316 batch_size = 10
1317 exception = None
1319 def _filter_ec2_with_volumes(self, instances):
1320 return [i for i in instances if len(i['BlockDeviceMappings']) > 0]
1322 def process(self, instances):
1323 instances = self._filter_ec2_with_volumes(
1324 self.filter_resources(instances, 'State.Name', self.valid_origin_states))
1325 if not len(instances):
1326 return
1328 client = utils.local_session(self.manager.session_factory).client('ec2')
1329 failures = {}
1331 # Play nice around aws having insufficient capacity...
1332 for itype, t_instances in utils.group_by(
1333 instances, 'InstanceType').items():
1334 for izone, z_instances in utils.group_by(
1335 t_instances, 'Placement.AvailabilityZone').items():
1336 for batch in utils.chunks(z_instances, self.batch_size):
1337 fails = self.process_instance_set(client, batch, itype, izone)
1338 if fails:
1339 failures["%s %s" % (itype, izone)] = [i['InstanceId'] for i in batch]
1341 if failures:
1342 fail_count = sum(map(len, failures.values()))
1343 msg = "Could not start %d of %d instances %s" % (
1344 fail_count, len(instances), utils.dumps(failures))
1345 self.log.warning(msg)
1346 raise RuntimeError(msg)
1348 def process_instance_set(self, client, instances, itype, izone):
1349 # Setup retry with insufficient capacity as well
1350 retryable = ('InsufficientInstanceCapacity', 'RequestLimitExceeded',
1351 'Client.RequestLimitExceeded', 'Server.InsufficientInstanceCapacity'),
1352 retry = utils.get_retry(retryable, max_attempts=5)
1353 instance_ids = [i['InstanceId'] for i in instances]
1354 while instance_ids:
1355 try:
1356 retry(client.start_instances, InstanceIds=instance_ids)
1357 break
1358 except ClientError as e:
1359 if e.response['Error']['Code'] in retryable:
1360 # we maxed out on our retries
1361 return True
1362 elif e.response['Error']['Code'] == 'IncorrectInstanceState':
1363 instance_ids.remove(extract_instance_id(e))
1364 else:
1365 raise
1368def extract_instance_id(state_error):
1369 "Extract an instance id from an error"
1370 instance_id = None
1371 match = RE_ERROR_INSTANCE_ID.search(str(state_error))
1372 if match:
1373 instance_id = match.groupdict().get('instance_id')
1374 if match is None or instance_id is None:
1375 raise ValueError("Could not extract instance id from error: %s" % state_error)
1376 return instance_id
1379@actions.register('resize')
1380class Resize(BaseAction):
1381 """Change an instance's size.
1383 An instance can only be resized when its stopped, this action
1384 can optionally stop/start an instance if needed to effect the instance
1385 type change. Instances are always left in the run state they were
1386 found in.
1388 There are a few caveats to be aware of, instance resizing
1389 needs to maintain compatibility for architecture, virtualization type
1390 hvm/pv, and ebs optimization at minimum.
1392 http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-resize.html
1394 This action also has specific support for enacting recommendations
1395 from the AWS Cost Optimization Hub for resizing.
1397 :example:
1399 .. code-block:: yaml
1401 policies:
1402 - name: ec2-rightsize
1403 resource: aws.ec2
1404 filters:
1405 - type: cost-optimization
1406 attrs:
1407 - actionType: Rightsize
1408 actions:
1409 - resize
1411 """
1413 schema = type_schema(
1414 'resize',
1415 **{'restart': {'type': 'boolean'},
1416 'type-map': {'type': 'object'},
1417 'default': {'type': 'string'}})
1419 valid_origin_states = ('running', 'stopped')
1421 def get_permissions(self):
1422 perms = ('ec2:DescribeInstances', 'ec2:ModifyInstanceAttribute')
1423 if self.data.get('restart', False):
1424 perms += ('ec2:StopInstances', 'ec2:StartInstances')
1425 return perms
1427 def process(self, resources):
1428 stopped_instances = self.filter_resources(resources, 'State.Name', ('stopped',))
1429 running_instances = self.filter_resources(resources, 'State.Name', ('running',))
1431 if self.data.get('restart') and running_instances:
1432 Stop({'terminate-ephemeral': False},
1433 self.manager).process(running_instances)
1434 client = utils.local_session(
1435 self.manager.session_factory).client('ec2')
1436 waiter = client.get_waiter('instance_stopped')
1437 try:
1438 waiter.wait(
1439 InstanceIds=[r['InstanceId'] for r in running_instances])
1440 except ClientError as e:
1441 self.log.exception(
1442 "Exception stopping instances for resize:\n %s" % e)
1444 client = utils.local_session(self.manager.session_factory).client('ec2')
1446 for instance_set in utils.chunks(itertools.chain(
1447 stopped_instances, running_instances), 20):
1448 self.process_resource_set(instance_set, client)
1450 if self.data.get('restart') and running_instances:
1451 client.start_instances(
1452 InstanceIds=[i['InstanceId'] for i in running_instances])
1453 return list(itertools.chain(stopped_instances, running_instances))
1455 def process_resource_set(self, instance_set, client):
1457 for i in instance_set:
1458 new_type = self.get_target_instance_type(i)
1459 self.log.debug(
1460 "resizing %s %s -> %s" % (i['InstanceId'], i['InstanceType'], new_type)
1461 )
1463 if not new_type or new_type == i['InstanceType']:
1464 continue
1465 try:
1466 client.modify_instance_attribute(
1467 InstanceId=i['InstanceId'],
1468 InstanceType={'Value': new_type})
1469 except ClientError as e:
1470 self.log.exception(
1471 "Exception resizing instance:%s new:%s old:%s \n %s" % (
1472 i['InstanceId'], new_type, i['InstanceType'], e))
1474 def get_target_instance_type(self, i):
1475 optimizer_recommend = i.get(CostHubRecommendation.annotation_key)
1476 if optimizer_recommend and optimizer_recommend['actionType'] == 'Rightsize':
1477 return optimizer_recommend['recommendedResourceSummary']
1478 type_map = self.data.get('type-map', {})
1479 default_type = self.data.get('default')
1480 return type_map.get(i['InstanceType'], default_type)
1483@actions.register('stop')
1484class Stop(BaseAction):
1485 """Stops or hibernates a running EC2 instances
1487 :Example:
1489 .. code-block:: yaml
1491 policies:
1492 - name: ec2-stop-running-instances
1493 resource: ec2
1494 query:
1495 - instance-state-name: running
1496 actions:
1497 - stop
1499 - name: ec2-hibernate-instances
1500 resources: ec2
1501 query:
1502 - instance-state-name: running
1503 actions:
1504 - type: stop
1505 hibernate: true
1508 Note when using hiberate, instances not configured for hiberation
1509 will just be stopped.
1510 """
1511 valid_origin_states = ('running',)
1513 schema = type_schema(
1514 'stop',
1515 **{
1516 "terminate-ephemeral": {"type": "boolean"},
1517 "hibernate": {"type": "boolean"},
1518 "force": {"type": "boolean"},
1519 },
1520 )
1522 has_hibernate = jmespath_compile('[].HibernationOptions.Configured')
1524 def get_permissions(self):
1525 perms = ('ec2:StopInstances',)
1526 if self.data.get('terminate-ephemeral', False):
1527 perms += ('ec2:TerminateInstances',)
1528 if self.data.get("force"):
1529 perms += ("ec2:ModifyInstanceAttribute",)
1530 return perms
1532 def split_on_storage(self, instances):
1533 ephemeral = []
1534 persistent = []
1535 for i in instances:
1536 if EphemeralInstanceFilter.is_ephemeral(i):
1537 ephemeral.append(i)
1538 else:
1539 persistent.append(i)
1540 return ephemeral, persistent
1542 def split_on_hibernate(self, instances):
1543 enabled, disabled = [], []
1544 for status, i in zip(self.has_hibernate.search(instances), instances):
1545 if status is True:
1546 enabled.append(i)
1547 else:
1548 disabled.append(i)
1549 return enabled, disabled
1551 def process(self, instances):
1552 instances = self.filter_resources(instances, 'State.Name', self.valid_origin_states)
1553 if not len(instances):
1554 return
1555 client = utils.local_session(
1556 self.manager.session_factory).client('ec2')
1557 # Ephemeral instance can't be stopped.
1558 ephemeral, persistent = self.split_on_storage(instances)
1559 if self.data.get('terminate-ephemeral', False) and ephemeral:
1560 self._run_instances_op(client, 'terminate', ephemeral)
1561 if persistent:
1562 if self.data.get('hibernate', False):
1563 enabled, persistent = self.split_on_hibernate(persistent)
1564 if enabled:
1565 self._run_instances_op(client, 'stop', enabled, Hibernate=True)
1566 self._run_instances_op(client, 'stop', persistent)
1567 return instances
1569 def disable_protection(self, client, op, instances):
1570 def modify_instance(i, attribute):
1571 try:
1572 self.manager.retry(
1573 client.modify_instance_attribute,
1574 InstanceId=i['InstanceId'],
1575 Attribute=attribute,
1576 Value='false',
1577 )
1578 except ClientError as e:
1579 if e.response['Error']['Code'] == 'IncorrectInstanceState':
1580 return
1581 raise
1583 def process_instance(i, op):
1584 modify_instance(i, 'disableApiStop')
1585 if op == 'terminate':
1586 modify_instance(i, 'disableApiTermination')
1588 with self.executor_factory(max_workers=2) as w:
1589 list(w.map(process_instance, instances, [op] * len(instances)))
1591 def _run_instances_op(self, client, op, instances, **kwargs):
1592 client_op = client.stop_instances
1593 if op == 'terminate':
1594 client_op = client.terminate_instances
1596 instance_ids = [i['InstanceId'] for i in instances]
1598 while instances:
1599 try:
1600 return self.manager.retry(client_op, InstanceIds=instance_ids, **kwargs)
1601 except ClientError as e:
1602 if e.response['Error']['Code'] == 'IncorrectInstanceState':
1603 instance_ids.remove(extract_instance_id(e))
1604 if (
1605 e.response['Error']['Code'] == 'OperationNotPermitted' and
1606 self.data.get('force')
1607 ):
1608 self.log.info("Disabling stop and termination protection on instances")
1609 self.disable_protection(
1610 client,
1611 op,
1612 [i for i in instances if i.get('InstanceLifecycle') != 'spot'],
1613 )
1614 continue
1615 raise
1618@actions.register('reboot')
1619class Reboot(BaseAction):
1620 """Reboots a previously running EC2 instance.
1622 :Example:
1624 .. code-block:: yaml
1626 policies:
1627 - name: ec2-reboot-instances
1628 resource: ec2
1629 query:
1630 - instance-state-name: running
1631 actions:
1632 - reboot
1634 http://docs.aws.amazon.com/cli/latest/reference/ec2/reboot-instances.html
1635 """
1637 valid_origin_states = ('running',)
1638 schema = type_schema('reboot')
1639 permissions = ('ec2:RebootInstances',)
1640 batch_size = 10
1641 exception = None
1643 def _filter_ec2_with_volumes(self, instances):
1644 return [i for i in instances if len(i['BlockDeviceMappings']) > 0]
1646 def process(self, instances):
1647 instances = self._filter_ec2_with_volumes(
1648 self.filter_resources(instances, 'State.Name', self.valid_origin_states))
1649 if not len(instances):
1650 return
1652 client = utils.local_session(self.manager.session_factory).client('ec2')
1653 failures = {}
1655 for batch in utils.chunks(instances, self.batch_size):
1656 fails = self.process_instance_set(client, batch)
1657 if fails:
1658 failures = [i['InstanceId'] for i in batch]
1660 if failures:
1661 fail_count = sum(map(len, failures.values()))
1662 msg = "Could not reboot %d of %d instances %s" % (
1663 fail_count, len(instances),
1664 utils.dumps(failures))
1665 self.log.warning(msg)
1666 raise RuntimeError(msg)
1668 def process_instance_set(self, client, instances):
1669 # Setup retry with insufficient capacity as well
1670 retryable = ('InsufficientInstanceCapacity', 'RequestLimitExceeded',
1671 'Client.RequestLimitExceeded'),
1672 retry = utils.get_retry(retryable, max_attempts=5)
1673 instance_ids = [i['InstanceId'] for i in instances]
1674 try:
1675 retry(client.reboot_instances, InstanceIds=instance_ids)
1676 except ClientError as e:
1677 if e.response['Error']['Code'] in retryable:
1678 return True
1679 raise
1682@actions.register('terminate')
1683class Terminate(BaseAction):
1684 """ Terminate a set of instances.
1686 While ec2 offers a bulk delete api, any given instance can be configured
1687 with api deletion termination protection, so we can't use the bulk call
1688 reliabily, we need to process the instances individually. Additionally
1689 If we're configured with 'force' then we'll turn off instance termination
1690 and stop protection.
1692 :Example:
1694 .. code-block:: yaml
1696 policies:
1697 - name: ec2-process-termination
1698 resource: ec2
1699 filters:
1700 - type: marked-for-op
1701 op: terminate
1702 actions:
1703 - terminate
1704 """
1706 valid_origin_states = ('running', 'stopped', 'pending', 'stopping')
1708 schema = type_schema('terminate', force={'type': 'boolean'})
1710 def get_permissions(self):
1711 permissions = ("ec2:TerminateInstances",)
1712 if self.data.get('force'):
1713 permissions += ('ec2:ModifyInstanceAttribute',)
1714 return permissions
1716 def process_terminate(self, instances):
1717 client = utils.local_session(
1718 self.manager.session_factory).client('ec2')
1719 try:
1720 self.manager.retry(
1721 client.terminate_instances,
1722 InstanceIds=[i['InstanceId'] for i in instances])
1723 return
1724 except ClientError as e:
1725 if e.response['Error']['Code'] != 'OperationNotPermitted':
1726 raise
1727 if not self.data.get('force'):
1728 raise
1730 self.log.info("Disabling stop and termination protection on instances")
1731 self.disable_deletion_protection(
1732 client,
1733 [i for i in instances if i.get('InstanceLifecycle') != 'spot'])
1734 self.manager.retry(
1735 client.terminate_instances,
1736 InstanceIds=[i['InstanceId'] for i in instances])
1738 def process(self, instances):
1739 instances = self.filter_resources(instances, 'State.Name', self.valid_origin_states)
1740 if not len(instances):
1741 return
1742 # limit batch sizes to avoid api limits
1743 for batch in utils.chunks(instances, 100):
1744 self.process_terminate(batch)
1746 def disable_deletion_protection(self, client, instances):
1748 def modify_instance(i, attribute):
1749 try:
1750 self.manager.retry(
1751 client.modify_instance_attribute,
1752 InstanceId=i['InstanceId'],
1753 Attribute=attribute,
1754 Value='false')
1755 except ClientError as e:
1756 if e.response['Error']['Code'] == 'IncorrectInstanceState':
1757 return
1758 raise
1760 def process_instance(i):
1761 modify_instance(i, 'disableApiTermination')
1762 modify_instance(i, 'disableApiStop')
1764 with self.executor_factory(max_workers=2) as w:
1765 list(w.map(process_instance, instances))
1768@actions.register('snapshot')
1769class Snapshot(BaseAction):
1770 """Snapshot the volumes attached to an EC2 instance.
1772 Tags may be optionally added to the snapshot during creation.
1774 - `copy-volume-tags` copies all the tags from the specified
1775 volume to the corresponding snapshot.
1776 - `copy-tags` copies the listed tags from each volume
1777 to the snapshot. This is mutually exclusive with
1778 `copy-volume-tags`.
1779 - `tags` allows new tags to be added to each snapshot when using
1780 'copy-tags`. If no tags are specified, then the tag
1781 `custodian_snapshot` is added.
1783 The default behavior is `copy-volume-tags: true`.
1785 :Example:
1787 .. code-block:: yaml
1789 policies:
1790 - name: ec2-snapshots
1791 resource: ec2
1792 actions:
1793 - type: snapshot
1794 copy-tags:
1795 - Name
1796 tags:
1797 custodian_snapshot: True
1798 """
1800 schema = type_schema(
1801 'snapshot',
1802 **{'copy-tags': {'type': 'array', 'items': {'type': 'string'}},
1803 'copy-volume-tags': {'type': 'boolean'},
1804 'tags': {'type': 'object'},
1805 'exclude-boot': {'type': 'boolean', 'default': False}})
1806 permissions = ('ec2:CreateSnapshot', 'ec2:CreateTags',)
1808 def validate(self):
1809 if self.data.get('copy-tags') and 'copy-volume-tags' in self.data:
1810 raise PolicyValidationError(
1811 "Can specify copy-tags or copy-volume-tags, not both")
1813 def process(self, resources):
1814 client = utils.local_session(self.manager.session_factory).client('ec2')
1815 err = None
1816 with self.executor_factory(max_workers=2) as w:
1817 futures = {}
1818 for resource in resources:
1819 futures[w.submit(
1820 self.process_volume_set, client, resource)] = resource
1821 for f in as_completed(futures):
1822 if f.exception():
1823 err = f.exception()
1824 resource = futures[f]
1825 self.log.error(
1826 "Exception creating snapshot set instance:%s \n %s" % (
1827 resource['InstanceId'], err))
1828 if err:
1829 raise err
1831 def get_instance_name(self, resource):
1832 tags = resource.get('Tags', [])
1833 for tag in tags:
1834 if tag['Key'] == 'Name':
1835 return tag['Value']
1836 return "-"
1838 def process_volume_set(self, client, resource):
1839 i_name = self.get_instance_name(resource)
1840 params = dict(
1841 Description=f"Snapshot Created for {resource['InstanceId']} ({i_name})",
1842 InstanceSpecification={
1843 'ExcludeBootVolume': self.data.get('exclude-boot', False),
1844 'InstanceId': resource['InstanceId']})
1845 if 'copy-tags' in self.data:
1846 params['TagSpecifications'] = [{
1847 'ResourceType': 'snapshot',
1848 'Tags': self.get_snapshot_tags(resource)}]
1849 elif self.data.get('copy-volume-tags', True):
1850 params['CopyTagsFromSource'] = 'volume'
1852 try:
1853 result = self.manager.retry(client.create_snapshots, **params)
1854 resource['c7n:snapshots'] = [
1855 s['SnapshotId'] for s in result['Snapshots']]
1856 except ClientError as e:
1857 err_code = e.response['Error']['Code']
1858 if err_code not in (
1859 'InvalidInstanceId.NotFound',
1860 'ConcurrentSnapshotLimitExceeded',
1861 'IncorrectState'):
1862 raise
1863 self.log.warning(
1864 "action:snapshot instance:%s error:%s",
1865 resource['InstanceId'], err_code)
1867 def get_snapshot_tags(self, resource):
1868 user_tags = self.data.get('tags', {}) or {'custodian_snapshot': ''}
1869 copy_tags = self.data.get('copy-tags', [])
1870 return coalesce_copy_user_tags(resource, copy_tags, user_tags)
1873@actions.register('modify-security-groups')
1874class EC2ModifyVpcSecurityGroups(ModifyVpcSecurityGroupsAction):
1875 """Modify security groups on an instance."""
1877 permissions = ("ec2:ModifyNetworkInterfaceAttribute",)
1878 sg_expr = jmespath_compile("Groups[].GroupId")
1880 def process(self, instances):
1881 if not len(instances):
1882 return
1883 client = utils.local_session(
1884 self.manager.session_factory).client('ec2')
1886 # handle multiple ENIs
1887 interfaces = []
1888 for i in instances:
1889 for eni in i['NetworkInterfaces']:
1890 if i.get('c7n:matched-security-groups'):
1891 eni['c7n:matched-security-groups'] = i[
1892 'c7n:matched-security-groups']
1893 if i.get('c7n:NetworkLocation'):
1894 eni['c7n:NetworkLocation'] = i[
1895 'c7n:NetworkLocation']
1896 interfaces.append(eni)
1898 groups = super(EC2ModifyVpcSecurityGroups, self).get_groups(interfaces)
1900 for idx, i in enumerate(interfaces):
1901 client.modify_network_interface_attribute(
1902 NetworkInterfaceId=i['NetworkInterfaceId'],
1903 Groups=groups[idx])
1906@actions.register('autorecover-alarm')
1907class AutorecoverAlarm(BaseAction):
1908 """Adds a cloudwatch metric alarm to recover an EC2 instance.
1910 This action takes effect on instances that are NOT part
1911 of an ASG.
1913 :Example:
1915 .. code-block:: yaml
1917 policies:
1918 - name: ec2-autorecover-alarm
1919 resource: ec2
1920 filters:
1921 - singleton
1922 actions:
1923 - autorecover-alarm
1925 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-recover.html
1926 """
1928 schema = type_schema('autorecover-alarm')
1929 permissions = ('cloudwatch:PutMetricAlarm',)
1930 valid_origin_states = ('running', 'stopped', 'pending', 'stopping')
1931 filter_asg_membership = ValueFilter({
1932 'key': 'tag:aws:autoscaling:groupName',
1933 'value': 'empty'}).validate()
1935 def process(self, instances):
1936 instances = self.filter_asg_membership.process(
1937 self.filter_resources(instances, 'State.Name', self.valid_origin_states))
1938 if not len(instances):
1939 return
1940 client = utils.local_session(
1941 self.manager.session_factory).client('cloudwatch')
1942 for i in instances:
1943 client.put_metric_alarm(
1944 AlarmName='recover-{}'.format(i['InstanceId']),
1945 AlarmDescription='Auto Recover {}'.format(i['InstanceId']),
1946 ActionsEnabled=True,
1947 AlarmActions=[
1948 'arn:{}:automate:{}:ec2:recover'.format(
1949 utils.REGION_PARTITION_MAP.get(
1950 self.manager.config.region, 'aws'),
1951 i['Placement']['AvailabilityZone'][:-1])
1952 ],
1953 MetricName='StatusCheckFailed_System',
1954 Namespace='AWS/EC2',
1955 Statistic='Minimum',
1956 Dimensions=[
1957 {
1958 'Name': 'InstanceId',
1959 'Value': i['InstanceId']
1960 }
1961 ],
1962 Period=60,
1963 EvaluationPeriods=2,
1964 Threshold=0,
1965 ComparisonOperator='GreaterThanThreshold'
1966 )
1969@actions.register('set-instance-profile')
1970class SetInstanceProfile(BaseAction):
1971 """Sets (add, modify, remove) the instance profile for a running EC2 instance.
1973 :Example:
1975 .. code-block:: yaml
1977 policies:
1978 - name: set-default-instance-profile
1979 resource: ec2
1980 filters:
1981 - IamInstanceProfile: absent
1982 actions:
1983 - type: set-instance-profile
1984 name: default
1986 https://docs.aws.amazon.com/cli/latest/reference/ec2/associate-iam-instance-profile.html
1987 https://docs.aws.amazon.com/cli/latest/reference/ec2/disassociate-iam-instance-profile.html
1988 """
1990 schema = type_schema(
1991 'set-instance-profile',
1992 **{'name': {'type': 'string'}})
1994 permissions = (
1995 'ec2:AssociateIamInstanceProfile',
1996 'ec2:DisassociateIamInstanceProfile',
1997 'iam:PassRole')
1999 valid_origin_states = ('running', 'pending', 'stopped', 'stopping')
2001 def process(self, instances):
2002 instances = self.filter_resources(instances, 'State.Name', self.valid_origin_states)
2003 if not len(instances):
2004 return
2005 client = utils.local_session(self.manager.session_factory).client('ec2')
2006 profile_name = self.data.get('name')
2007 profile_instances = [i for i in instances if i.get('IamInstanceProfile')]
2009 if profile_instances:
2010 associations = {
2011 a['InstanceId']: (a['AssociationId'], a['IamInstanceProfile']['Arn'])
2012 for a in client.describe_iam_instance_profile_associations(
2013 Filters=[
2014 {'Name': 'instance-id',
2015 'Values': [i['InstanceId'] for i in profile_instances]},
2016 {'Name': 'state', 'Values': ['associating', 'associated']}]
2017 ).get('IamInstanceProfileAssociations', ())}
2018 else:
2019 associations = {}
2021 for i in instances:
2022 if profile_name and i['InstanceId'] not in associations:
2023 client.associate_iam_instance_profile(
2024 IamInstanceProfile={'Name': profile_name},
2025 InstanceId=i['InstanceId'])
2026 continue
2027 # Removing profile and no profile on instance.
2028 elif profile_name is None and i['InstanceId'] not in associations:
2029 continue
2031 p_assoc_id, p_arn = associations[i['InstanceId']]
2033 # Already associated to target profile, skip
2034 if profile_name and p_arn.endswith('/%s' % profile_name):
2035 continue
2037 if profile_name is None:
2038 client.disassociate_iam_instance_profile(
2039 AssociationId=p_assoc_id)
2040 else:
2041 client.replace_iam_instance_profile_association(
2042 IamInstanceProfile={'Name': profile_name},
2043 AssociationId=p_assoc_id)
2045 return instances
2048@actions.register('propagate-spot-tags')
2049class PropagateSpotTags(BaseAction):
2050 """Propagate Tags that are set at Spot Request level to EC2 instances.
2052 :Example:
2054 .. code-block:: yaml
2056 policies:
2057 - name: ec2-spot-instances
2058 resource: ec2
2059 filters:
2060 - State.Name: pending
2061 - instanceLifecycle: spot
2062 actions:
2063 - type: propagate-spot-tags
2064 only_tags:
2065 - Name
2066 - BillingTag
2067 """
2069 schema = type_schema(
2070 'propagate-spot-tags',
2071 **{'only_tags': {'type': 'array', 'items': {'type': 'string'}}})
2073 permissions = (
2074 'ec2:DescribeInstances',
2075 'ec2:DescribeSpotInstanceRequests',
2076 'ec2:DescribeTags',
2077 'ec2:CreateTags')
2079 MAX_TAG_COUNT = 50
2081 def process(self, instances):
2082 instances = [
2083 i for i in instances if i['InstanceLifecycle'] == 'spot']
2084 if not len(instances):
2085 self.log.warning(
2086 "action:%s no spot instances found, implicit filter by action" % (
2087 self.__class__.__name__.lower()))
2088 return
2090 client = utils.local_session(
2091 self.manager.session_factory).client('ec2')
2093 request_instance_map = {}
2094 for i in instances:
2095 request_instance_map.setdefault(
2096 i['SpotInstanceRequestId'], []).append(i)
2098 # ... and describe the corresponding spot requests ...
2099 requests = client.describe_spot_instance_requests(
2100 Filters=[{
2101 'Name': 'spot-instance-request-id',
2102 'Values': list(request_instance_map.keys())}]).get(
2103 'SpotInstanceRequests', [])
2105 updated = []
2106 for r in requests:
2107 if not r.get('Tags'):
2108 continue
2109 updated.extend(
2110 self.process_request_instances(
2111 client, r, request_instance_map[r['SpotInstanceRequestId']]))
2112 return updated
2114 def process_request_instances(self, client, request, instances):
2115 # Now we find the tags we can copy : either all, either those
2116 # indicated with 'only_tags' parameter.
2117 copy_keys = self.data.get('only_tags', [])
2118 request_tags = {t['Key']: t['Value'] for t in request['Tags']
2119 if not t['Key'].startswith('aws:')}
2120 if copy_keys:
2121 for k in set(copy_keys).difference(request_tags):
2122 del request_tags[k]
2124 update_instances = []
2125 for i in instances:
2126 instance_tags = {t['Key']: t['Value'] for t in i.get('Tags', [])}
2127 # We may overwrite tags, but if the operation changes no tag,
2128 # we will not proceed.
2129 for k, v in request_tags.items():
2130 if k not in instance_tags or instance_tags[k] != v:
2131 update_instances.append(i['InstanceId'])
2133 if len(set(instance_tags) | set(request_tags)) > self.MAX_TAG_COUNT:
2134 self.log.warning(
2135 "action:%s instance:%s too many tags to copy (> 50)" % (
2136 self.__class__.__name__.lower(),
2137 i['InstanceId']))
2138 continue
2140 for iset in utils.chunks(update_instances, 20):
2141 client.create_tags(
2142 DryRun=self.manager.config.dryrun,
2143 Resources=iset,
2144 Tags=[{'Key': k, 'Value': v} for k, v in request_tags.items()])
2146 self.log.debug(
2147 "action:%s tags updated on instances:%r" % (
2148 self.__class__.__name__.lower(),
2149 update_instances))
2151 return update_instances
2154class EC2QueryParser(QueryParser):
2156 # Valid EC2 Query Filters
2157 # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2/client/describe_instances.html
2158 QuerySchema = {
2159 'Filters': {
2160 'architecture': ('i386', 'x86_64'),
2161 'availability-zone': str,
2162 'ebs-optimized': str,
2163 'iam-instance-profile.arn': str,
2164 'image-id': str,
2165 'instance-id': str,
2166 'instance-lifecycle': ('spot', 'scheduled', 'capacity-block'),
2167 'instance-state-name': (
2168 'pending',
2169 'terminated',
2170 'running',
2171 'shutting-down',
2172 'stopping',
2173 'stopped'),
2174 'instance-type': str,
2175 'instance.group-id': str,
2176 'instance.group-name': str,
2177 'owner-id': str,
2178 'platform': str,
2179 'tag-key': str,
2180 'tag-value': str,
2181 'tenancy': ('dedicated', 'default', 'host'),
2182 'vpc-id': str
2183 },
2184 'InstanceIds': str,
2185 'MaxResults': int,
2186 }
2187 single_value_fields = ('MaxResults',)
2189 type_name = "EC2"
2192@filters.register('instance-attribute')
2193class InstanceAttribute(ValueFilter):
2194 """EC2 Instance Value Filter on a given instance attribute.
2196 Filters EC2 Instances with the given instance attribute
2198 :Example:
2200 .. code-block:: yaml
2202 policies:
2203 - name: ec2-unoptimized-ebs
2204 resource: ec2
2205 filters:
2206 - type: instance-attribute
2207 attribute: ebsOptimized
2208 key: "Value"
2209 value: false
2210 """
2212 valid_attrs = (
2213 'instanceType',
2214 'kernel',
2215 'ramdisk',
2216 'userData',
2217 'disableApiTermination',
2218 'instanceInitiatedShutdownBehavior',
2219 'rootDeviceName',
2220 'blockDeviceMapping',
2221 'productCodes',
2222 'sourceDestCheck',
2223 'groupSet',
2224 'ebsOptimized',
2225 'sriovNetSupport',
2226 'enaSupport')
2228 schema = type_schema(
2229 'instance-attribute',
2230 rinherit=ValueFilter.schema,
2231 attribute={'enum': valid_attrs},
2232 required=('attribute',))
2233 schema_alias = False
2235 def get_permissions(self):
2236 return ('ec2:DescribeInstanceAttribute',)
2238 def process(self, resources, event=None):
2239 attribute = self.data['attribute']
2240 self.get_instance_attribute(resources, attribute)
2241 return [resource for resource in resources
2242 if self.match(resource['c7n:attribute-%s' % attribute])]
2244 def get_instance_attribute(self, resources, attribute):
2245 client = utils.local_session(
2246 self.manager.session_factory).client('ec2')
2248 for resource in resources:
2249 instance_id = resource['InstanceId']
2250 fetched_attribute = self.manager.retry(
2251 client.describe_instance_attribute,
2252 Attribute=attribute,
2253 InstanceId=instance_id)
2254 keys = list(fetched_attribute.keys())
2255 keys.remove('ResponseMetadata')
2256 keys.remove('InstanceId')
2257 resource['c7n:attribute-%s' % attribute] = fetched_attribute[
2258 keys[0]]
2261@resources.register('launch-template-version')
2262class LaunchTemplate(query.QueryResourceManager):
2264 class resource_type(query.TypeInfo):
2265 id = 'LaunchTemplateId'
2266 id_prefix = 'lt-'
2267 name = 'LaunchTemplateName'
2268 service = 'ec2'
2269 date = 'CreateTime'
2270 enum_spec = (
2271 'describe_launch_templates', 'LaunchTemplates', None)
2272 filter_name = 'LaunchTemplateIds'
2273 filter_type = 'list'
2274 arn_type = "launch-template"
2275 cfn_type = "AWS::EC2::LaunchTemplate"
2277 def augment(self, resources):
2278 client = utils.local_session(
2279 self.session_factory).client('ec2')
2280 template_versions = []
2281 for r in resources:
2282 template_versions.extend(
2283 client.describe_launch_template_versions(
2284 LaunchTemplateId=r['LaunchTemplateId']).get(
2285 'LaunchTemplateVersions', ()))
2286 return template_versions
2288 def get_arns(self, resources):
2289 arns = []
2290 for r in resources:
2291 arns.append(self.generate_arn(f"{r['LaunchTemplateId']}/{r['VersionNumber']}"))
2292 return arns
2294 def get_resources(self, rids, cache=True):
2295 # Launch template versions have a compound primary key
2296 #
2297 # Support one of four forms of resource ids:
2298 #
2299 # - array of launch template ids
2300 # - array of tuples (launch template id, version id)
2301 # - array of dicts (with LaunchTemplateId and VersionNumber)
2302 # - array of dicts (with LaunchTemplateId and LatestVersionNumber)
2303 #
2304 # If an alias version is given $Latest, $Default, the alias will be
2305 # preserved as an annotation on the returned object 'c7n:VersionAlias'
2306 if not rids:
2307 return []
2309 t_versions = {}
2310 if isinstance(rids[0], tuple):
2311 for tid, tversion in rids:
2312 t_versions.setdefault(tid, []).append(tversion)
2313 elif isinstance(rids[0], dict):
2314 for tinfo in rids:
2315 t_versions.setdefault(
2316 tinfo['LaunchTemplateId'], []).append(
2317 tinfo.get('VersionNumber', tinfo.get('LatestVersionNumber')))
2318 elif isinstance(rids[0], str):
2319 for tid in rids:
2320 t_versions[tid] = []
2322 client = utils.local_session(self.session_factory).client('ec2')
2324 results = []
2325 # We may end up fetching duplicates on $Latest and $Version
2326 for tid, tversions in t_versions.items():
2327 try:
2328 ltv = client.describe_launch_template_versions(
2329 LaunchTemplateId=tid, Versions=tversions).get(
2330 'LaunchTemplateVersions')
2331 except ClientError as e:
2332 if e.response['Error']['Code'] == "InvalidLaunchTemplateId.NotFound":
2333 continue
2334 if e.response['Error']['Code'] == "InvalidLaunchTemplateId.VersionNotFound":
2335 continue
2336 raise
2337 if not tversions:
2338 tversions = [str(t['VersionNumber']) for t in ltv]
2339 for tversion, t in zip(tversions, ltv):
2340 if not tversion.isdigit():
2341 t['c7n:VersionAlias'] = tversion
2342 results.append(t)
2343 return results
2345 def get_asg_templates(self, asgs):
2346 templates = {}
2347 for a in asgs:
2348 t = None
2349 if 'LaunchTemplate' in a:
2350 t = a['LaunchTemplate']
2351 elif 'MixedInstancesPolicy' in a:
2352 t = a['MixedInstancesPolicy'][
2353 'LaunchTemplate']['LaunchTemplateSpecification']
2354 if t is None:
2355 continue
2356 templates.setdefault(
2357 (t['LaunchTemplateId'],
2358 t.get('Version', '$Default')), []).append(a['AutoScalingGroupName'])
2359 return templates
2362@resources.register('ec2-reserved')
2363class ReservedInstance(query.QueryResourceManager):
2365 class resource_type(query.TypeInfo):
2366 service = 'ec2'
2367 name = id = 'ReservedInstancesId'
2368 id_prefix = ""
2369 date = 'Start'
2370 enum_spec = (
2371 'describe_reserved_instances', 'ReservedInstances', None)
2372 filter_name = 'ReservedInstancesIds'
2373 filter_type = 'list'
2374 arn_type = "reserved-instances"
2377@resources.register('ec2-host')
2378class DedicatedHost(query.QueryResourceManager):
2379 """Custodian resource for managing EC2 Dedicated Hosts.
2380 """
2382 class resource_type(query.TypeInfo):
2383 service = 'ec2'
2384 name = id = 'HostId'
2385 id_prefix = 'h-'
2386 enum_spec = ('describe_hosts', 'Hosts', None)
2387 arn_type = "dedicated-host"
2388 filter_name = 'HostIds'
2389 filter_type = 'list'
2390 date = 'AllocationTime'
2391 cfn_type = config_type = 'AWS::EC2::Host'
2392 permissions_enum = ('ec2:DescribeHosts',)
2395@resources.register('ec2-spot-fleet-request')
2396class SpotFleetRequest(query.QueryResourceManager):
2397 """Custodian resource for managing EC2 Spot Fleet Requests.
2398 """
2400 class resource_type(query.TypeInfo):
2401 service = 'ec2'
2402 name = id = 'SpotFleetRequestId'
2403 id_prefix = 'sfr-'
2404 enum_spec = ('describe_spot_fleet_requests', 'SpotFleetRequestConfigs', None)
2405 filter_name = 'SpotFleetRequestIds'
2406 filter_type = 'list'
2407 date = 'CreateTime'
2408 arn_type = 'spot-fleet-request'
2409 config_type = cfn_type = 'AWS::EC2::SpotFleet'
2410 permissions_enum = ('ec2:DescribeSpotFleetRequests',)
2413SpotFleetRequest.filter_registry.register('offhour', OffHour)
2414SpotFleetRequest.filter_registry.register('onhour', OnHour)
2417@SpotFleetRequest.action_registry.register('resize')
2418class AutoscalingSpotFleetRequest(AutoscalingBase):
2419 permissions = (
2420 'ec2:CreateTags',
2421 'ec2:ModifySpotFleetRequest',
2422 )
2424 service_namespace = 'ec2'
2425 scalable_dimension = 'ec2:spot-fleet-request:TargetCapacity'
2427 def get_resource_id(self, resource):
2428 return 'spot-fleet-request/%s' % resource['SpotFleetRequestId']
2430 def get_resource_tag(self, resource, key):
2431 if 'Tags' in resource:
2432 for tag in resource['Tags']:
2433 if tag['Key'] == key:
2434 return tag['Value']
2435 return None
2437 def get_resource_desired(self, resource):
2438 return int(resource['SpotFleetRequestConfig']['TargetCapacity'])
2440 def set_resource_desired(self, resource, desired):
2441 client = utils.local_session(self.manager.session_factory).client('ec2')
2442 client.modify_spot_fleet_request(
2443 SpotFleetRequestId=resource['SpotFleetRequestId'],
2444 TargetCapacity=desired,
2445 )
2448@EC2.filter_registry.register('has-specific-managed-policy')
2449class HasSpecificManagedPolicy(SpecificIamProfileManagedPolicy):
2450 """Filter an EC2 instance that has an IAM instance profile that contains an IAM role that has
2451 a specific managed IAM policy. If an EC2 instance does not have a profile or the profile
2452 does not contain an IAM role, then it will be treated as not having the policy.
2454 :example:
2456 .. code-block:: yaml
2458 policies:
2459 - name: ec2-instance-has-admin-policy
2460 resource: aws.ec2
2461 filters:
2462 - type: has-specific-managed-policy
2463 value: admin-policy
2465 :example:
2467 Check for EC2 instances with instance profile roles that have an
2468 attached policy matching a given list:
2470 .. code-block:: yaml
2472 policies:
2473 - name: ec2-instance-with-selected-policies
2474 resource: aws.ec2
2475 filters:
2476 - type: has-specific-managed-policy
2477 op: in
2478 value:
2479 - AmazonS3FullAccess
2480 - AWSOrganizationsFullAccess
2482 :example:
2484 Check for EC2 instances with instance profile roles that have
2485 attached policy names matching a pattern:
2487 .. code-block:: yaml
2489 policies:
2490 - name: ec2-instance-with-full-access-policies
2491 resource: aws.ec2
2492 filters:
2493 - type: has-specific-managed-policy
2494 op: glob
2495 value: "*FullAccess"
2497 Check for EC2 instances with instance profile roles that have
2498 attached policy ARNs matching a pattern:
2500 .. code-block:: yaml
2502 policies:
2503 - name: ec2-instance-with-aws-full-access-policies
2504 resource: aws.ec2
2505 filters:
2506 - type: has-specific-managed-policy
2507 key: PolicyArn
2508 op: regex
2509 value: "arn:aws:iam::aws:policy/.*FullAccess"
2510 """
2512 permissions = (
2513 'iam:GetInstanceProfile',
2514 'iam:ListInstanceProfiles',
2515 'iam:ListAttachedRolePolicies')
2517 def process(self, resources, event=None):
2518 client = utils.local_session(self.manager.session_factory).client('iam')
2519 iam_profiles = self.manager.get_resource_manager('iam-profile').resources()
2520 iam_profiles_mapping = {profile['Arn']: profile for profile in iam_profiles}
2522 results = []
2523 for r in resources:
2524 if r['State']['Name'] == 'terminated':
2525 continue
2526 instance_profile_arn = r.get('IamInstanceProfile', {}).get('Arn')
2527 if not instance_profile_arn:
2528 continue
2530 profile = iam_profiles_mapping.get(instance_profile_arn)
2531 if not profile:
2532 continue
2534 self.get_managed_policies(client, [profile])
2536 matched_keys = [k for k in profile[self.annotation_key] if self.match(k)]
2537 self.merge_annotation(profile, self.matched_annotation_key, matched_keys)
2538 if matched_keys:
2539 results.append(r)
2541 return results
2544@resources.register('ec2-capacity-reservation')
2545class CapacityReservation(query.QueryResourceManager):
2546 """Custodian resource for managing EC2 Capacity Reservation.
2547 """
2549 class resource_type(query.TypeInfo):
2550 name = id = 'CapacityReservationId'
2551 service = 'ec2'
2552 enum_spec = ('describe_capacity_reservations',
2553 'CapacityReservations', None)
2555 id_prefix = 'cr-'
2556 arn = "CapacityReservationArn"
2557 filter_name = 'CapacityReservationIds'
2558 filter_type = 'list'
2559 cfn_type = 'AWS::EC2::CapacityReservation'
2560 permissions_enum = ('ec2:DescribeCapacityReservations',)