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
11
12import botocore
13from botocore.exceptions import ClientError
14from dateutil.parser import parse
15from concurrent.futures import as_completed
16
17from c7n.actions import (
18 ActionRegistry, BaseAction, ModifyVpcSecurityGroupsAction, AutoscalingBase
19)
20
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
28
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
33
34from c7n.resources.iam import CheckPermissions, SpecificIamProfileManagedPolicy
35from c7n.resources.securityhub import PostFinding
36
37RE_ERROR_INSTANCE_ID = re.compile("'(?P<instance_id>i-.*?)'")
38
39filters = FilterRegistry('ec2.filters')
40actions = ActionRegistry('ec2.actions')
41
42
43class DescribeEC2(query.DescribeSource):
44
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
60
61 def augment(self, resources):
62 """EC2 API and AWOL Tags
63
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.
67
68 See footnote on for official documentation.
69 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#Using_Tags_CLI
70
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
81
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
92
93 if found:
94 return resources
95
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)
107
108 m = self.manager.get_model()
109 for r in resources:
110 r['Tags'] = resource_tags.get(r[m.id], [])
111 return resources
112
113
114@resources.register('ec2')
115class EC2(query.QueryResourceManager):
116
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',)
130
131 default_report_fields = (
132 'CustodianDate',
133 'InstanceId',
134 'tag:Name',
135 'InstanceType',
136 'LaunchTime',
137 'VpcId',
138 'PrivateIpAddress',
139 )
140
141 filter_registry = filters
142 action_registry = actions
143
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 }
150
151
152@filters.register('security-group')
153class SecurityGroupFilter(net_filters.SecurityGroupFilter):
154
155 RelatedIdsExpression = "NetworkInterfaces[].Groups[].GroupId"
156
157
158@filters.register('subnet')
159class SubnetFilter(net_filters.SubnetFilter):
160
161 RelatedIdsExpression = "NetworkInterfaces[].SubnetId"
162
163
164@filters.register('vpc')
165class VpcFilter(net_filters.VpcFilter):
166
167 RelatedIdsExpression = "VpcId"
168
169
170@filters.register('check-permissions')
171class ComputePermissions(CheckPermissions):
172
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}
177
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]
190
191
192@filters.register('state-age')
193class StateTransitionAge(AgeFilter):
194 """Age an instance has been in the given state.
195
196 .. code-block:: yaml
197
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"\(.*?\)")
207
208 # this filter doesn't use date_attribute, but needs to define it
209 # to pass AgeFilter's validate method
210 date_attribute = "dummy"
211
212 schema = type_schema(
213 'state-age',
214 op={'$ref': '#/definitions/filters_common/comparison_operators'},
215 days={'type': 'number'})
216
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
225
226
227@filters.register('ebs')
228class AttachedVolume(ValueFilter):
229 """EC2 instances with EBS backed volume
230
231 Filters EC2 instances with EBS backed storage devices (non ephemeral)
232
233 :Example:
234
235 .. code-block:: yaml
236
237 policies:
238 - name: ec2-encrypted-ebs-volumes
239 resource: ec2
240 filters:
241 - type: ebs
242 key: Encrypted
243 value: true
244 """
245
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
251
252 def get_permissions(self):
253 return self.manager.get_resource_manager('ebs').get_permissions()
254
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))
261
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
278
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))
289
290
291@filters.register('stop-protected')
292class DisableApiStop(Filter):
293 """EC2 instances with ``disableApiStop`` attribute set
294
295 Filters EC2 instances with ``disableApiStop`` attribute set to true.
296
297 :Example:
298
299 .. code-block:: yaml
300
301 policies:
302 - name: stop-protection-enabled
303 resource: ec2
304 filters:
305 - type: stop-protected
306
307 :Example:
308
309 .. code-block:: yaml
310
311 policies:
312 - name: stop-protection-NOT-enabled
313 resource: ec2
314 filters:
315 - not:
316 - type: stop-protected
317 """
318
319 schema = type_schema('stop-protected')
320 permissions = ('ec2:DescribeInstanceAttribute',)
321
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)]
327
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']
335
336 def validate(self) -> None:
337 botocore_min_version = '1.26.7'
338
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 )
345
346
347@filters.register('termination-protected')
348class DisableApiTermination(Filter):
349 """EC2 instances with ``disableApiTermination`` attribute set
350
351 Filters EC2 instances with ``disableApiTermination`` attribute set to true.
352
353 :Example:
354
355 .. code-block:: yaml
356
357 policies:
358 - name: termination-protection-enabled
359 resource: ec2
360 filters:
361 - type: termination-protected
362
363 :Example:
364
365 .. code-block:: yaml
366
367 policies:
368 - name: termination-protection-NOT-enabled
369 resource: ec2
370 filters:
371 - not:
372 - type: termination-protected
373 """
374
375 schema = type_schema('termination-protected')
376 permissions = ('ec2:DescribeInstanceAttribute',)
377
378 def get_permissions(self):
379 perms = list(self.permissions)
380 perms.extend(self.manager.get_permissions())
381 return perms
382
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)]
388
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']
396
397
398class InstanceImageBase:
399
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)
403
404 def get_base_image_mapping(self):
405 return {i['ImageId']: i for i in
406 self.manager.get_resource_manager('ami').resources()}
407
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
413
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
422
423
424@filters.register('image-age')
425class ImageAge(AgeFilter, InstanceImageBase):
426 """EC2 AMI age filter
427
428 Filters EC2 instances based on the age of their AMI image (in days)
429
430 :Example:
431
432 .. code-block:: yaml
433
434 policies:
435 - name: ec2-ancient-ami
436 resource: ec2
437 filters:
438 - type: image-age
439 op: ge
440 days: 90
441 """
442
443 date_attribute = "CreationDate"
444
445 schema = type_schema(
446 'image-age',
447 op={'$ref': '#/definitions/filters_common/comparison_operators'},
448 days={'type': 'number'})
449
450 def get_permissions(self):
451 return self.manager.get_resource_manager('ami').get_permissions()
452
453 def process(self, resources, event=None):
454 self.prefetch_instance_images(resources)
455 return super(ImageAge, self).process(resources, event)
456
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")
463
464
465@filters.register('image')
466class InstanceImage(ValueFilter, InstanceImageBase):
467
468 schema = type_schema('image', rinherit=ValueFilter.schema)
469 schema_alias = False
470
471 def get_permissions(self):
472 return self.manager.get_resource_manager('ami').get_permissions()
473
474 def process(self, resources, event=None):
475 self.prefetch_instance_images(resources)
476 return super(InstanceImage, self).process(resources, event)
477
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)
488
489
490@filters.register('offhour')
491class InstanceOffHour(OffHour):
492 """Custodian OffHour filter
493
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.
498
499 Note: You can disable filtering of only running instances by setting
500 `state-filter: false`
501
502 :Example:
503
504 .. code-block:: yaml
505
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
516
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
527
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 """
542
543 schema = type_schema(
544 'offhour', rinherit=OffHour.schema,
545 **{'state-filter': {'type': 'boolean'}})
546 schema_alias = False
547
548 valid_origin_states = ('running',)
549
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)
556
557
558@filters.register('network-location')
559class EC2NetworkLocation(net_filters.NetworkLocation):
560
561 valid_origin_states = ('pending', 'running', 'shutting-down', 'stopping',
562 'stopped')
563
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)
569
570
571@filters.register('onhour')
572class InstanceOnHour(OnHour):
573 """Custodian OnHour filter
574
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.
579
580 Note: You can disable filtering of only stopped instances by setting
581 `state-filter: false`
582
583 :Example:
584
585 .. code-block:: yaml
586
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
597
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
608
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 """
623
624 schema = type_schema(
625 'onhour', rinherit=OnHour.schema,
626 **{'state-filter': {'type': 'boolean'}})
627 schema_alias = False
628
629 valid_origin_states = ('stopped',)
630
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)
637
638
639@filters.register('ephemeral')
640class EphemeralInstanceFilter(Filter):
641 """EC2 instances with ephemeral storage
642
643 Filters EC2 instances that have ephemeral storage (an instance-store backed
644 root device)
645
646 :Example:
647
648 .. code-block:: yaml
649
650 policies:
651 - name: ec2-ephemeral-instances
652 resource: ec2
653 filters:
654 - type: ephemeral
655
656 http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html
657 """
658
659 schema = type_schema('ephemeral')
660
661 def __call__(self, i):
662 return self.is_ephemeral(i)
663
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
672
673
674@filters.register('instance-uptime')
675class UpTimeFilter(AgeFilter):
676
677 date_attribute = "LaunchTime"
678
679 schema = type_schema(
680 'instance-uptime',
681 op={'$ref': '#/definitions/filters_common/comparison_operators'},
682 days={'type': 'number'})
683
684
685@filters.register('instance-age')
686class InstanceAgeFilter(AgeFilter):
687 """Filters instances based on their age (in days)
688
689 :Example:
690
691 .. code-block:: yaml
692
693 policies:
694 - name: ec2-30-days-plus
695 resource: ec2
696 filters:
697 - type: instance-age
698 op: ge
699 days: 30
700 """
701
702 date_attribute = "LaunchTime"
703 ebs_key_func = operator.itemgetter('AttachTime')
704
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'})
711
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']
724
725
726@filters.register('default-vpc')
727class DefaultVpc(net_filters.DefaultVpcBase):
728 """ Matches if an ec2 database is in the default vpc
729 """
730
731 schema = type_schema('default-vpc')
732
733 def __call__(self, ec2):
734 return ec2.get('VpcId') and self.match(ec2.get('VpcId')) or False
735
736
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')
744
745
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.
751
752 :example:
753
754 .. code-block:: yaml
755
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 """
766
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',)
772
773 def __init__(self, data, manager):
774 super(UserData, self).__init__(data, manager)
775 self.data['key'] = '"c7n:user-data"'
776
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
786
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
793
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
813
814
815@filters.register('singleton')
816class SingletonFilter(Filter):
817 """EC2 instances without autoscaling or a recover alarm
818
819 Filters EC2 instances that are not members of an autoscaling group
820 and do not have Cloudwatch recover alarms.
821
822 :Example:
823
824 .. code-block:: yaml
825
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
835
836 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-recover.html
837 """
838
839 schema = type_schema('singleton')
840
841 permissions = ('cloudwatch:DescribeAlarmsForMetric',)
842
843 valid_origin_states = ('running', 'stopped', 'pending', 'stopping')
844
845 in_asg = ValueFilter({
846 'key': 'tag:aws:autoscaling:groupName',
847 'value': 'not-null'}).validate()
848
849 def process(self, instances, event=None):
850 return super(SingletonFilter, self).process(
851 self.filter_resources(instances, 'State.Name', self.valid_origin_states))
852
853 def __call__(self, i):
854 if self.in_asg(i):
855 return False
856 else:
857 return not self.has_recover_alarm(i)
858
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 )
871
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
879
880 return False
881
882
883@EC2.filter_registry.register('ssm')
884class SsmStatus(ValueFilter):
885 """Filter ec2 instances by their ssm status information.
886
887 :Example:
888
889 Find ubuntu 18.04 instances are active with ssm.
890
891 .. code-block:: yaml
892
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'
911
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
922
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'], {})
932
933
934@EC2.filter_registry.register('ssm-inventory')
935class SsmInventory(Filter):
936 """Filter EC2 instances by their SSM software inventory.
937
938 :Example:
939
940 Find instances that have a specific package installed.
941
942 .. code-block:: yaml
943
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
954
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']}}})
970
971 permissions = ('ssm:ListInventoryEntries',)
972 annotation_key = 'c7n:SSM-Inventory'
973
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
1001
1002
1003@EC2.filter_registry.register('ssm-compliance')
1004class SsmCompliance(Filter):
1005 """Filter ec2 instances by their ssm compliance status.
1006
1007 :Example:
1008
1009 Find non-compliant ec2 instances.
1010
1011 .. code-block:: yaml
1012
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'
1055
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)
1063
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 })
1085
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)
1098
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)
1105
1106 return results
1107
1108
1109@actions.register('set-monitoring')
1110class MonitorInstances(BaseAction):
1111 """Action on EC2 Instances to enable/disable detailed monitoring
1112
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)
1116
1117 :Example:
1118
1119 .. code-block:: yaml
1120
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
1129
1130 References
1131
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')
1137
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)
1147
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
1156
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
1165
1166
1167@EC2.action_registry.register('set-metadata-access')
1168class SetMetadataServerAccess(BaseAction):
1169 """Set instance metadata server access for an instance.
1170
1171 :example:
1172
1173 Require instances to use IMDSv2
1174
1175 .. code-block:: yaml
1176
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
1185
1186 :example:
1187
1188 Disable metadata server access
1189
1190 .. code-block: yaml
1191
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
1200
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
1209
1210 Reference: https://amzn.to/2XOuxpQ
1211 """
1212
1213 AllowedValues = {
1214 'HttpEndpoint': ['enabled', 'disabled'],
1215 'HttpTokens': ['required', 'optional'],
1216 'InstanceMetadataTags': ['enabled', 'disabled'],
1217 'HttpPutResponseHopLimit': list(range(1, 65))
1218 }
1219
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',)
1232
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')})
1239
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)
1247
1248 if not resources:
1249 return
1250
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)
1258
1259
1260@EC2.action_registry.register("post-finding")
1261class InstanceFinding(PostFinding):
1262
1263 resource_type = 'AwsEc2Instance'
1264
1265 def format_resource(self, r):
1266 ip_addresses = jmespath_search(
1267 "NetworkInterfaces[].PrivateIpAddresses[].PrivateIpAddress", r)
1268
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 }
1278
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"]
1286
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 }
1298
1299 instance = filter_empty(instance)
1300 return instance
1301
1302
1303@actions.register('start')
1304class Start(BaseAction):
1305 """Starts a previously stopped EC2 instance.
1306
1307 :Example:
1308
1309 .. code-block:: yaml
1310
1311 policies:
1312 - name: ec2-start-stopped-instances
1313 resource: ec2
1314 query:
1315 - instance-state-name: stopped
1316 actions:
1317 - start
1318
1319 http://docs.aws.amazon.com/cli/latest/reference/ec2/start-instances.html
1320 """
1321
1322 valid_origin_states = ('stopped',)
1323 schema = type_schema('start')
1324 permissions = ('ec2:StartInstances',)
1325 batch_size = 10
1326 exception = None
1327
1328 def _filter_ec2_with_volumes(self, instances):
1329 return [i for i in instances if len(i['BlockDeviceMappings']) > 0]
1330
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
1336
1337 client = utils.local_session(self.manager.session_factory).client('ec2')
1338 failures = {}
1339
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]
1349
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)
1356
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
1375
1376
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
1386
1387
1388@actions.register('resize')
1389class Resize(BaseAction):
1390 """Change an instance's size.
1391
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.
1396
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.
1400
1401 http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-resize.html
1402
1403 This action also has specific support for enacting recommendations
1404 from the AWS Cost Optimization Hub for resizing.
1405
1406 :example:
1407
1408 .. code-block:: yaml
1409
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
1419
1420 """
1421
1422 schema = type_schema(
1423 'resize',
1424 **{'restart': {'type': 'boolean'},
1425 'type-map': {'type': 'object'},
1426 'default': {'type': 'string'}})
1427
1428 valid_origin_states = ('running', 'stopped')
1429
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
1435
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',))
1439
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)
1452
1453 client = utils.local_session(self.manager.session_factory).client('ec2')
1454
1455 for instance_set in utils.chunks(itertools.chain(
1456 stopped_instances, running_instances), 20):
1457 self.process_resource_set(instance_set, client)
1458
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))
1463
1464 def process_resource_set(self, instance_set, client):
1465
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 )
1471
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))
1482
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)
1490
1491
1492@actions.register('stop')
1493class Stop(BaseAction):
1494 """Stops or hibernates a running EC2 instances
1495
1496 :Example:
1497
1498 .. code-block:: yaml
1499
1500 policies:
1501 - name: ec2-stop-running-instances
1502 resource: ec2
1503 query:
1504 - instance-state-name: running
1505 actions:
1506 - stop
1507
1508 - name: ec2-hibernate-instances
1509 resources: ec2
1510 query:
1511 - instance-state-name: running
1512 actions:
1513 - type: stop
1514 hibernate: true
1515
1516
1517 Note when using hiberate, instances not configured for hiberation
1518 will just be stopped.
1519 """
1520 valid_origin_states = ('running',)
1521
1522 schema = type_schema(
1523 'stop',
1524 **{
1525 "terminate-ephemeral": {"type": "boolean"},
1526 "hibernate": {"type": "boolean"},
1527 "force": {"type": "boolean"},
1528 },
1529 )
1530
1531 has_hibernate = jmespath_compile('[].HibernationOptions.Configured')
1532
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
1540
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
1550
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
1559
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
1577
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
1591
1592 def process_instance(i, op):
1593 modify_instance(i, 'disableApiStop')
1594 if op == 'terminate':
1595 modify_instance(i, 'disableApiTermination')
1596
1597 with self.executor_factory(max_workers=2) as w:
1598 list(w.map(process_instance, instances, [op] * len(instances)))
1599
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
1604
1605 instance_ids = [i['InstanceId'] for i in instances]
1606
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
1625
1626
1627@actions.register('reboot')
1628class Reboot(BaseAction):
1629 """Reboots a previously running EC2 instance.
1630
1631 :Example:
1632
1633 .. code-block:: yaml
1634
1635 policies:
1636 - name: ec2-reboot-instances
1637 resource: ec2
1638 query:
1639 - instance-state-name: running
1640 actions:
1641 - reboot
1642
1643 http://docs.aws.amazon.com/cli/latest/reference/ec2/reboot-instances.html
1644 """
1645
1646 valid_origin_states = ('running',)
1647 schema = type_schema('reboot')
1648 permissions = ('ec2:RebootInstances',)
1649 batch_size = 10
1650 exception = None
1651
1652 def _filter_ec2_with_volumes(self, instances):
1653 return [i for i in instances if len(i['BlockDeviceMappings']) > 0]
1654
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
1660
1661 client = utils.local_session(self.manager.session_factory).client('ec2')
1662 failures = {}
1663
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]
1668
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)
1676
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
1689
1690
1691@actions.register('terminate')
1692class Terminate(BaseAction):
1693 """ Terminate a set of instances.
1694
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.
1700
1701 :Example:
1702
1703 .. code-block:: yaml
1704
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 """
1714
1715 valid_origin_states = ('running', 'stopped', 'pending', 'stopping')
1716
1717 schema = type_schema('terminate', force={'type': 'boolean'})
1718
1719 def get_permissions(self):
1720 permissions = ("ec2:TerminateInstances",)
1721 if self.data.get('force'):
1722 permissions += ('ec2:ModifyInstanceAttribute',)
1723 return permissions
1724
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
1738
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])
1746
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)
1754
1755 def disable_deletion_protection(self, client, instances):
1756
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
1768
1769 def process_instance(i):
1770 modify_instance(i, 'disableApiTermination')
1771 modify_instance(i, 'disableApiStop')
1772
1773 with self.executor_factory(max_workers=2) as w:
1774 list(w.map(process_instance, instances))
1775
1776
1777@actions.register('snapshot')
1778class Snapshot(BaseAction):
1779 """Snapshot the volumes attached to an EC2 instance.
1780
1781 Tags may be optionally added to the snapshot during creation.
1782
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.
1791
1792 The default behavior is `copy-volume-tags: true`.
1793
1794 :Example:
1795
1796 .. code-block:: yaml
1797
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 """
1808
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',)
1816
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")
1821
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
1839
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 "-"
1846
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'
1860
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)
1875
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)
1880
1881
1882@actions.register('modify-security-groups')
1883class EC2ModifyVpcSecurityGroups(ModifyVpcSecurityGroupsAction):
1884 """Modify security groups on an instance."""
1885
1886 permissions = ("ec2:ModifyNetworkInterfaceAttribute",)
1887 sg_expr = jmespath_compile("Groups[].GroupId")
1888
1889 def process(self, instances):
1890 if not len(instances):
1891 return
1892 client = utils.local_session(
1893 self.manager.session_factory).client('ec2')
1894
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)
1906
1907 groups = super(EC2ModifyVpcSecurityGroups, self).get_groups(interfaces)
1908
1909 for idx, i in enumerate(interfaces):
1910 client.modify_network_interface_attribute(
1911 NetworkInterfaceId=i['NetworkInterfaceId'],
1912 Groups=groups[idx])
1913
1914
1915@actions.register('autorecover-alarm')
1916class AutorecoverAlarm(BaseAction):
1917 """Adds a cloudwatch metric alarm to recover an EC2 instance.
1918
1919 This action takes effect on instances that are NOT part
1920 of an ASG.
1921
1922 :Example:
1923
1924 .. code-block:: yaml
1925
1926 policies:
1927 - name: ec2-autorecover-alarm
1928 resource: ec2
1929 filters:
1930 - singleton
1931 actions:
1932 - autorecover-alarm
1933
1934 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-recover.html
1935 """
1936
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()
1943
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 )
1976
1977
1978@actions.register('set-instance-profile')
1979class SetInstanceProfile(BaseAction):
1980 """Sets (add, modify, remove) the instance profile for a running EC2 instance.
1981
1982 :Example:
1983
1984 .. code-block:: yaml
1985
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
1994
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 """
1998
1999 schema = type_schema(
2000 'set-instance-profile',
2001 **{'name': {'type': 'string'}})
2002
2003 permissions = (
2004 'ec2:AssociateIamInstanceProfile',
2005 'ec2:DisassociateIamInstanceProfile',
2006 'iam:PassRole')
2007
2008 valid_origin_states = ('running', 'pending', 'stopped', 'stopping')
2009
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')]
2017
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 = {}
2029
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
2039
2040 p_assoc_id, p_arn = associations[i['InstanceId']]
2041
2042 # Already associated to target profile, skip
2043 if profile_name and p_arn.endswith('/%s' % profile_name):
2044 continue
2045
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)
2053
2054 return instances
2055
2056
2057@actions.register('propagate-spot-tags')
2058class PropagateSpotTags(BaseAction):
2059 """Propagate Tags that are set at Spot Request level to EC2 instances.
2060
2061 :Example:
2062
2063 .. code-block:: yaml
2064
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 """
2077
2078 schema = type_schema(
2079 'propagate-spot-tags',
2080 **{'only_tags': {'type': 'array', 'items': {'type': 'string'}}})
2081
2082 permissions = (
2083 'ec2:DescribeInstances',
2084 'ec2:DescribeSpotInstanceRequests',
2085 'ec2:DescribeTags',
2086 'ec2:CreateTags')
2087
2088 MAX_TAG_COUNT = 50
2089
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
2098
2099 client = utils.local_session(
2100 self.manager.session_factory).client('ec2')
2101
2102 request_instance_map = {}
2103 for i in instances:
2104 request_instance_map.setdefault(
2105 i['SpotInstanceRequestId'], []).append(i)
2106
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', [])
2113
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
2122
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]
2132
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'])
2141
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
2148
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()])
2154
2155 self.log.debug(
2156 "action:%s tags updated on instances:%r" % (
2157 self.__class__.__name__.lower(),
2158 update_instances))
2159
2160 return update_instances
2161
2162
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}
2186
2187
2188class QueryFilter:
2189
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
2199
2200 def __init__(self, data):
2201 self.data = data
2202 self.key = None
2203 self.value = None
2204
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]
2211
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))
2216
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
2223
2224 def query(self):
2225 value = self.value
2226 if isinstance(self.value, str):
2227 value = [self.value]
2228
2229 return {'Name': self.key, 'Values': value}
2230
2231
2232@filters.register('instance-attribute')
2233class InstanceAttribute(ValueFilter):
2234 """EC2 Instance Value Filter on a given instance attribute.
2235
2236 Filters EC2 Instances with the given instance attribute
2237
2238 :Example:
2239
2240 .. code-block:: yaml
2241
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 """
2251
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')
2267
2268 schema = type_schema(
2269 'instance-attribute',
2270 rinherit=ValueFilter.schema,
2271 attribute={'enum': valid_attrs},
2272 required=('attribute',))
2273 schema_alias = False
2274
2275 def get_permissions(self):
2276 return ('ec2:DescribeInstanceAttribute',)
2277
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])]
2283
2284 def get_instance_attribute(self, resources, attribute):
2285 client = utils.local_session(
2286 self.manager.session_factory).client('ec2')
2287
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]]
2299
2300
2301@resources.register('launch-template-version')
2302class LaunchTemplate(query.QueryResourceManager):
2303
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"
2316
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
2327
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
2333
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 []
2348
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] = []
2361
2362 client = utils.local_session(self.session_factory).client('ec2')
2363
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
2384
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
2400
2401
2402@resources.register('ec2-reserved')
2403class ReservedInstance(query.QueryResourceManager):
2404
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"
2415
2416
2417@resources.register('ec2-host')
2418class DedicatedHost(query.QueryResourceManager):
2419 """Custodian resource for managing EC2 Dedicated Hosts.
2420 """
2421
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',)
2433
2434
2435@resources.register('ec2-spot-fleet-request')
2436class SpotFleetRequest(query.QueryResourceManager):
2437 """Custodian resource for managing EC2 Spot Fleet Requests.
2438 """
2439
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',)
2451
2452
2453SpotFleetRequest.filter_registry.register('offhour', OffHour)
2454SpotFleetRequest.filter_registry.register('onhour', OnHour)
2455
2456
2457@SpotFleetRequest.action_registry.register('resize')
2458class AutoscalingSpotFleetRequest(AutoscalingBase):
2459 permissions = (
2460 'ec2:CreateTags',
2461 'ec2:ModifySpotFleetRequest',
2462 )
2463
2464 service_namespace = 'ec2'
2465 scalable_dimension = 'ec2:spot-fleet-request:TargetCapacity'
2466
2467 def get_resource_id(self, resource):
2468 return 'spot-fleet-request/%s' % resource['SpotFleetRequestId']
2469
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
2476
2477 def get_resource_desired(self, resource):
2478 return int(resource['SpotFleetRequestConfig']['TargetCapacity'])
2479
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 )
2486
2487
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.
2493
2494 :example:
2495
2496 .. code-block:: yaml
2497
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
2504
2505 :example:
2506
2507 Check for EC2 instances with instance profile roles that have an
2508 attached policy matching a given list:
2509
2510 .. code-block:: yaml
2511
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
2521
2522 :example:
2523
2524 Check for EC2 instances with instance profile roles that have
2525 attached policy names matching a pattern:
2526
2527 .. code-block:: yaml
2528
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"
2536
2537 Check for EC2 instances with instance profile roles that have
2538 attached policy ARNs matching a pattern:
2539
2540 .. code-block:: yaml
2541
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 """
2551
2552 permissions = (
2553 'iam:GetInstanceProfile',
2554 'iam:ListInstanceProfiles',
2555 'iam:ListAttachedRolePolicies')
2556
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}
2561
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
2569
2570 profile = iam_profiles_mapping.get(instance_profile_arn)
2571 if not profile:
2572 continue
2573
2574 self.get_managed_policies(client, [profile])
2575
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)
2580
2581 return results
2582
2583
2584@resources.register('ec2-capacity-reservation')
2585class CapacityReservation(query.QueryResourceManager):
2586 """Custodian resource for managing EC2 Capacity Reservation.
2587 """
2588
2589 class resource_type(query.TypeInfo):
2590 name = id = 'CapacityReservationId'
2591 service = 'ec2'
2592 enum_spec = ('describe_capacity_reservations',
2593 'CapacityReservations', None)
2594
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',)