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

1048 statements  

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, QueryParser 

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 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 

51 

52 def augment(self, resources): 

53 """EC2 API and AWOL Tags 

54 

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. 

58 

59 See footnote on for official documentation. 

60 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#Using_Tags_CLI 

61 

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 

72 

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 

83 

84 if found: 

85 return resources 

86 

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) 

98 

99 m = self.manager.get_model() 

100 for r in resources: 

101 r['Tags'] = resource_tags.get(r[m.id], []) 

102 return resources 

103 

104 

105@resources.register('ec2') 

106class EC2(query.QueryResourceManager): 

107 

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',) 

121 

122 default_report_fields = ( 

123 'CustodianDate', 

124 'InstanceId', 

125 'tag:Name', 

126 'InstanceType', 

127 'LaunchTime', 

128 'VpcId', 

129 'PrivateIpAddress', 

130 ) 

131 

132 filter_registry = filters 

133 action_registry = actions 

134 

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 } 

141 

142 

143@filters.register('security-group') 

144class SecurityGroupFilter(net_filters.SecurityGroupFilter): 

145 

146 RelatedIdsExpression = "NetworkInterfaces[].Groups[].GroupId" 

147 

148 

149@filters.register('subnet') 

150class SubnetFilter(net_filters.SubnetFilter): 

151 

152 RelatedIdsExpression = "NetworkInterfaces[].SubnetId" 

153 

154 

155@filters.register('vpc') 

156class VpcFilter(net_filters.VpcFilter): 

157 

158 RelatedIdsExpression = "VpcId" 

159 

160 

161@filters.register('check-permissions') 

162class ComputePermissions(CheckPermissions): 

163 

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} 

168 

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] 

181 

182 

183@filters.register('state-age') 

184class StateTransitionAge(AgeFilter): 

185 """Age an instance has been in the given state. 

186 

187 .. code-block:: yaml 

188 

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"\(.*?\)") 

198 

199 # this filter doesn't use date_attribute, but needs to define it 

200 # to pass AgeFilter's validate method 

201 date_attribute = "dummy" 

202 

203 schema = type_schema( 

204 'state-age', 

205 op={'$ref': '#/definitions/filters_common/comparison_operators'}, 

206 days={'type': 'number'}) 

207 

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 

216 

217 

218@filters.register('ebs') 

219class AttachedVolume(ValueFilter): 

220 """EC2 instances with EBS backed volume 

221 

222 Filters EC2 instances with EBS backed storage devices (non ephemeral) 

223 

224 :Example: 

225 

226 .. code-block:: yaml 

227 

228 policies: 

229 - name: ec2-encrypted-ebs-volumes 

230 resource: ec2 

231 filters: 

232 - type: ebs 

233 key: Encrypted 

234 value: true 

235 """ 

236 

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 

242 

243 def get_permissions(self): 

244 return self.manager.get_resource_manager('ebs').get_permissions() 

245 

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)) 

252 

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 

269 

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)) 

280 

281 

282@filters.register('stop-protected') 

283class DisableApiStop(Filter): 

284 """EC2 instances with ``disableApiStop`` attribute set 

285 

286 Filters EC2 instances with ``disableApiStop`` attribute set to true. 

287 

288 :Example: 

289 

290 .. code-block:: yaml 

291 

292 policies: 

293 - name: stop-protection-enabled 

294 resource: ec2 

295 filters: 

296 - type: stop-protected 

297 

298 :Example: 

299 

300 .. code-block:: yaml 

301 

302 policies: 

303 - name: stop-protection-NOT-enabled 

304 resource: ec2 

305 filters: 

306 - not: 

307 - type: stop-protected 

308 """ 

309 

310 schema = type_schema('stop-protected') 

311 permissions = ('ec2:DescribeInstanceAttribute',) 

312 

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)] 

318 

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'] 

326 

327 def validate(self) -> None: 

328 botocore_min_version = '1.26.7' 

329 

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 ) 

336 

337 

338@filters.register('termination-protected') 

339class DisableApiTermination(Filter): 

340 """EC2 instances with ``disableApiTermination`` attribute set 

341 

342 Filters EC2 instances with ``disableApiTermination`` attribute set to true. 

343 

344 :Example: 

345 

346 .. code-block:: yaml 

347 

348 policies: 

349 - name: termination-protection-enabled 

350 resource: ec2 

351 filters: 

352 - type: termination-protected 

353 

354 :Example: 

355 

356 .. code-block:: yaml 

357 

358 policies: 

359 - name: termination-protection-NOT-enabled 

360 resource: ec2 

361 filters: 

362 - not: 

363 - type: termination-protected 

364 """ 

365 

366 schema = type_schema('termination-protected') 

367 permissions = ('ec2:DescribeInstanceAttribute',) 

368 

369 def get_permissions(self): 

370 perms = list(self.permissions) 

371 perms.extend(self.manager.get_permissions()) 

372 return perms 

373 

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)] 

379 

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'] 

387 

388 

389class InstanceImageBase: 

390 

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) 

394 

395 def get_base_image_mapping(self): 

396 return {i['ImageId']: i for i in 

397 self.manager.get_resource_manager('ami').resources()} 

398 

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 

404 

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 

413 

414 

415@filters.register('image-age') 

416class ImageAge(AgeFilter, InstanceImageBase): 

417 """EC2 AMI age filter 

418 

419 Filters EC2 instances based on the age of their AMI image (in days) 

420 

421 :Example: 

422 

423 .. code-block:: yaml 

424 

425 policies: 

426 - name: ec2-ancient-ami 

427 resource: ec2 

428 filters: 

429 - type: image-age 

430 op: ge 

431 days: 90 

432 """ 

433 

434 date_attribute = "CreationDate" 

435 

436 schema = type_schema( 

437 'image-age', 

438 op={'$ref': '#/definitions/filters_common/comparison_operators'}, 

439 days={'type': 'number'}) 

440 

441 def get_permissions(self): 

442 return self.manager.get_resource_manager('ami').get_permissions() 

443 

444 def process(self, resources, event=None): 

445 self.prefetch_instance_images(resources) 

446 return super(ImageAge, self).process(resources, event) 

447 

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") 

454 

455 

456@filters.register('image') 

457class InstanceImage(ValueFilter, InstanceImageBase): 

458 

459 schema = type_schema('image', rinherit=ValueFilter.schema) 

460 schema_alias = False 

461 

462 def get_permissions(self): 

463 return self.manager.get_resource_manager('ami').get_permissions() 

464 

465 def process(self, resources, event=None): 

466 self.prefetch_instance_images(resources) 

467 return super(InstanceImage, self).process(resources, event) 

468 

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) 

479 

480 

481@filters.register('offhour') 

482class InstanceOffHour(OffHour): 

483 """Custodian OffHour filter 

484 

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. 

489 

490 Note: You can disable filtering of only running instances by setting 

491 `state-filter: false` 

492 

493 :Example: 

494 

495 .. code-block:: yaml 

496 

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 

507 

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 

518 

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 """ 

533 

534 schema = type_schema( 

535 'offhour', rinherit=OffHour.schema, 

536 **{'state-filter': {'type': 'boolean'}}) 

537 schema_alias = False 

538 

539 valid_origin_states = ('running',) 

540 

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) 

547 

548 

549@filters.register('network-location') 

550class EC2NetworkLocation(net_filters.NetworkLocation): 

551 

552 valid_origin_states = ('pending', 'running', 'shutting-down', 'stopping', 

553 'stopped') 

554 

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) 

560 

561 

562@filters.register('onhour') 

563class InstanceOnHour(OnHour): 

564 """Custodian OnHour filter 

565 

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. 

570 

571 Note: You can disable filtering of only stopped instances by setting 

572 `state-filter: false` 

573 

574 :Example: 

575 

576 .. code-block:: yaml 

577 

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 

588 

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 

599 

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 """ 

614 

615 schema = type_schema( 

616 'onhour', rinherit=OnHour.schema, 

617 **{'state-filter': {'type': 'boolean'}}) 

618 schema_alias = False 

619 

620 valid_origin_states = ('stopped',) 

621 

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) 

628 

629 

630@filters.register('ephemeral') 

631class EphemeralInstanceFilter(Filter): 

632 """EC2 instances with ephemeral storage 

633 

634 Filters EC2 instances that have ephemeral storage (an instance-store backed 

635 root device) 

636 

637 :Example: 

638 

639 .. code-block:: yaml 

640 

641 policies: 

642 - name: ec2-ephemeral-instances 

643 resource: ec2 

644 filters: 

645 - type: ephemeral 

646 

647 http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html 

648 """ 

649 

650 schema = type_schema('ephemeral') 

651 

652 def __call__(self, i): 

653 return self.is_ephemeral(i) 

654 

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 

663 

664 

665@filters.register('instance-uptime') 

666class UpTimeFilter(AgeFilter): 

667 

668 date_attribute = "LaunchTime" 

669 

670 schema = type_schema( 

671 'instance-uptime', 

672 op={'$ref': '#/definitions/filters_common/comparison_operators'}, 

673 days={'type': 'number'}) 

674 

675 

676@filters.register('instance-age') 

677class InstanceAgeFilter(AgeFilter): 

678 """Filters instances based on their age (in days) 

679 

680 :Example: 

681 

682 .. code-block:: yaml 

683 

684 policies: 

685 - name: ec2-30-days-plus 

686 resource: ec2 

687 filters: 

688 - type: instance-age 

689 op: ge 

690 days: 30 

691 """ 

692 

693 date_attribute = "LaunchTime" 

694 ebs_key_func = operator.itemgetter('AttachTime') 

695 

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'}) 

702 

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'] 

715 

716 

717@filters.register('default-vpc') 

718class DefaultVpc(net_filters.DefaultVpcBase): 

719 """ Matches if an ec2 database is in the default vpc 

720 """ 

721 

722 schema = type_schema('default-vpc') 

723 

724 def __call__(self, ec2): 

725 return ec2.get('VpcId') and self.match(ec2.get('VpcId')) or False 

726 

727 

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') 

735 

736 

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. 

742 

743 :example: 

744 

745 .. code-block:: yaml 

746 

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 """ 

757 

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',) 

763 

764 def __init__(self, data, manager): 

765 super(UserData, self).__init__(data, manager) 

766 self.data['key'] = '"c7n:user-data"' 

767 

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 

777 

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 

784 

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 

804 

805 

806@filters.register('singleton') 

807class SingletonFilter(Filter): 

808 """EC2 instances without autoscaling or a recover alarm 

809 

810 Filters EC2 instances that are not members of an autoscaling group 

811 and do not have Cloudwatch recover alarms. 

812 

813 :Example: 

814 

815 .. code-block:: yaml 

816 

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 

826 

827 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-recover.html 

828 """ 

829 

830 schema = type_schema('singleton') 

831 

832 permissions = ('cloudwatch:DescribeAlarmsForMetric',) 

833 

834 valid_origin_states = ('running', 'stopped', 'pending', 'stopping') 

835 

836 in_asg = ValueFilter({ 

837 'key': 'tag:aws:autoscaling:groupName', 

838 'value': 'not-null'}).validate() 

839 

840 def process(self, instances, event=None): 

841 return super(SingletonFilter, self).process( 

842 self.filter_resources(instances, 'State.Name', self.valid_origin_states)) 

843 

844 def __call__(self, i): 

845 if self.in_asg(i): 

846 return False 

847 else: 

848 return not self.has_recover_alarm(i) 

849 

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 ) 

862 

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 

870 

871 return False 

872 

873 

874@EC2.filter_registry.register('ssm') 

875class SsmStatus(ValueFilter): 

876 """Filter ec2 instances by their ssm status information. 

877 

878 :Example: 

879 

880 Find ubuntu 18.04 instances are active with ssm. 

881 

882 .. code-block:: yaml 

883 

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' 

902 

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 

913 

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'], {}) 

923 

924 

925@EC2.filter_registry.register('ssm-inventory') 

926class SsmInventory(Filter): 

927 """Filter EC2 instances by their SSM software inventory. 

928 

929 :Example: 

930 

931 Find instances that have a specific package installed. 

932 

933 .. code-block:: yaml 

934 

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 

945 

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']}}}) 

961 

962 permissions = ('ssm:ListInventoryEntries',) 

963 annotation_key = 'c7n:SSM-Inventory' 

964 

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 

992 

993 

994@EC2.filter_registry.register('ssm-compliance') 

995class SsmCompliance(Filter): 

996 """Filter ec2 instances by their ssm compliance status. 

997 

998 :Example: 

999 

1000 Find non-compliant ec2 instances. 

1001 

1002 .. code-block:: yaml 

1003 

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' 

1046 

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) 

1054 

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 }) 

1076 

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) 

1089 

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) 

1096 

1097 return results 

1098 

1099 

1100@actions.register('set-monitoring') 

1101class MonitorInstances(BaseAction): 

1102 """Action on EC2 Instances to enable/disable detailed monitoring 

1103 

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) 

1107 

1108 :Example: 

1109 

1110 .. code-block:: yaml 

1111 

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 

1120 

1121 References 

1122 

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') 

1128 

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) 

1138 

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 

1147 

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 

1156 

1157 

1158@EC2.action_registry.register('set-metadata-access') 

1159class SetMetadataServerAccess(BaseAction): 

1160 """Set instance metadata server access for an instance. 

1161 

1162 :example: 

1163 

1164 Require instances to use IMDSv2 

1165 

1166 .. code-block:: yaml 

1167 

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 

1176 

1177 :example: 

1178 

1179 Disable metadata server access 

1180 

1181 .. code-block: yaml 

1182 

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 

1191 

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 

1200 

1201 Reference: https://amzn.to/2XOuxpQ 

1202 """ 

1203 

1204 AllowedValues = { 

1205 'HttpEndpoint': ['enabled', 'disabled'], 

1206 'HttpTokens': ['required', 'optional'], 

1207 'InstanceMetadataTags': ['enabled', 'disabled'], 

1208 'HttpPutResponseHopLimit': list(range(1, 65)) 

1209 } 

1210 

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',) 

1223 

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')}) 

1230 

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) 

1238 

1239 if not resources: 

1240 return 

1241 

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) 

1249 

1250 

1251@EC2.action_registry.register("post-finding") 

1252class InstanceFinding(PostFinding): 

1253 

1254 resource_type = 'AwsEc2Instance' 

1255 

1256 def format_resource(self, r): 

1257 ip_addresses = jmespath_search( 

1258 "NetworkInterfaces[].PrivateIpAddresses[].PrivateIpAddress", r) 

1259 

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 } 

1269 

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"] 

1277 

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 } 

1289 

1290 instance = filter_empty(instance) 

1291 return instance 

1292 

1293 

1294@actions.register('start') 

1295class Start(BaseAction): 

1296 """Starts a previously stopped EC2 instance. 

1297 

1298 :Example: 

1299 

1300 .. code-block:: yaml 

1301 

1302 policies: 

1303 - name: ec2-start-stopped-instances 

1304 resource: ec2 

1305 query: 

1306 - instance-state-name: stopped 

1307 actions: 

1308 - start 

1309 

1310 http://docs.aws.amazon.com/cli/latest/reference/ec2/start-instances.html 

1311 """ 

1312 

1313 valid_origin_states = ('stopped',) 

1314 schema = type_schema('start') 

1315 permissions = ('ec2:StartInstances',) 

1316 batch_size = 10 

1317 exception = None 

1318 

1319 def _filter_ec2_with_volumes(self, instances): 

1320 return [i for i in instances if len(i['BlockDeviceMappings']) > 0] 

1321 

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 

1327 

1328 client = utils.local_session(self.manager.session_factory).client('ec2') 

1329 failures = {} 

1330 

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] 

1340 

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) 

1347 

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 

1366 

1367 

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 

1377 

1378 

1379@actions.register('resize') 

1380class Resize(BaseAction): 

1381 """Change an instance's size. 

1382 

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. 

1387 

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. 

1391 

1392 http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-resize.html 

1393 

1394 This action also has specific support for enacting recommendations 

1395 from the AWS Cost Optimization Hub for resizing. 

1396 

1397 :example: 

1398 

1399 .. code-block:: yaml 

1400 

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 

1410 

1411 """ 

1412 

1413 schema = type_schema( 

1414 'resize', 

1415 **{'restart': {'type': 'boolean'}, 

1416 'type-map': {'type': 'object'}, 

1417 'default': {'type': 'string'}}) 

1418 

1419 valid_origin_states = ('running', 'stopped') 

1420 

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 

1426 

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',)) 

1430 

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) 

1443 

1444 client = utils.local_session(self.manager.session_factory).client('ec2') 

1445 

1446 for instance_set in utils.chunks(itertools.chain( 

1447 stopped_instances, running_instances), 20): 

1448 self.process_resource_set(instance_set, client) 

1449 

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)) 

1454 

1455 def process_resource_set(self, instance_set, client): 

1456 

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 ) 

1462 

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)) 

1473 

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) 

1481 

1482 

1483@actions.register('stop') 

1484class Stop(BaseAction): 

1485 """Stops or hibernates a running EC2 instances 

1486 

1487 :Example: 

1488 

1489 .. code-block:: yaml 

1490 

1491 policies: 

1492 - name: ec2-stop-running-instances 

1493 resource: ec2 

1494 query: 

1495 - instance-state-name: running 

1496 actions: 

1497 - stop 

1498 

1499 - name: ec2-hibernate-instances 

1500 resources: ec2 

1501 query: 

1502 - instance-state-name: running 

1503 actions: 

1504 - type: stop 

1505 hibernate: true 

1506 

1507 

1508 Note when using hiberate, instances not configured for hiberation 

1509 will just be stopped. 

1510 """ 

1511 valid_origin_states = ('running',) 

1512 

1513 schema = type_schema( 

1514 'stop', 

1515 **{ 

1516 "terminate-ephemeral": {"type": "boolean"}, 

1517 "hibernate": {"type": "boolean"}, 

1518 "force": {"type": "boolean"}, 

1519 }, 

1520 ) 

1521 

1522 has_hibernate = jmespath_compile('[].HibernationOptions.Configured') 

1523 

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 

1531 

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 

1541 

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 

1550 

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 

1568 

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 

1582 

1583 def process_instance(i, op): 

1584 modify_instance(i, 'disableApiStop') 

1585 if op == 'terminate': 

1586 modify_instance(i, 'disableApiTermination') 

1587 

1588 with self.executor_factory(max_workers=2) as w: 

1589 list(w.map(process_instance, instances, [op] * len(instances))) 

1590 

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 

1595 

1596 instance_ids = [i['InstanceId'] for i in instances] 

1597 

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 

1616 

1617 

1618@actions.register('reboot') 

1619class Reboot(BaseAction): 

1620 """Reboots a previously running EC2 instance. 

1621 

1622 :Example: 

1623 

1624 .. code-block:: yaml 

1625 

1626 policies: 

1627 - name: ec2-reboot-instances 

1628 resource: ec2 

1629 query: 

1630 - instance-state-name: running 

1631 actions: 

1632 - reboot 

1633 

1634 http://docs.aws.amazon.com/cli/latest/reference/ec2/reboot-instances.html 

1635 """ 

1636 

1637 valid_origin_states = ('running',) 

1638 schema = type_schema('reboot') 

1639 permissions = ('ec2:RebootInstances',) 

1640 batch_size = 10 

1641 exception = None 

1642 

1643 def _filter_ec2_with_volumes(self, instances): 

1644 return [i for i in instances if len(i['BlockDeviceMappings']) > 0] 

1645 

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 

1651 

1652 client = utils.local_session(self.manager.session_factory).client('ec2') 

1653 failures = {} 

1654 

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] 

1659 

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) 

1667 

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 

1680 

1681 

1682@actions.register('terminate') 

1683class Terminate(BaseAction): 

1684 """ Terminate a set of instances. 

1685 

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. 

1691 

1692 :Example: 

1693 

1694 .. code-block:: yaml 

1695 

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 """ 

1705 

1706 valid_origin_states = ('running', 'stopped', 'pending', 'stopping') 

1707 

1708 schema = type_schema('terminate', force={'type': 'boolean'}) 

1709 

1710 def get_permissions(self): 

1711 permissions = ("ec2:TerminateInstances",) 

1712 if self.data.get('force'): 

1713 permissions += ('ec2:ModifyInstanceAttribute',) 

1714 return permissions 

1715 

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 

1729 

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]) 

1737 

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) 

1745 

1746 def disable_deletion_protection(self, client, instances): 

1747 

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 

1759 

1760 def process_instance(i): 

1761 modify_instance(i, 'disableApiTermination') 

1762 modify_instance(i, 'disableApiStop') 

1763 

1764 with self.executor_factory(max_workers=2) as w: 

1765 list(w.map(process_instance, instances)) 

1766 

1767 

1768@actions.register('snapshot') 

1769class Snapshot(BaseAction): 

1770 """Snapshot the volumes attached to an EC2 instance. 

1771 

1772 Tags may be optionally added to the snapshot during creation. 

1773 

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. 

1782 

1783 The default behavior is `copy-volume-tags: true`. 

1784 

1785 :Example: 

1786 

1787 .. code-block:: yaml 

1788 

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 """ 

1799 

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',) 

1807 

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") 

1812 

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 

1830 

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 "-" 

1837 

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' 

1851 

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) 

1866 

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) 

1871 

1872 

1873@actions.register('modify-security-groups') 

1874class EC2ModifyVpcSecurityGroups(ModifyVpcSecurityGroupsAction): 

1875 """Modify security groups on an instance.""" 

1876 

1877 permissions = ("ec2:ModifyNetworkInterfaceAttribute",) 

1878 sg_expr = jmespath_compile("Groups[].GroupId") 

1879 

1880 def process(self, instances): 

1881 if not len(instances): 

1882 return 

1883 client = utils.local_session( 

1884 self.manager.session_factory).client('ec2') 

1885 

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) 

1897 

1898 groups = super(EC2ModifyVpcSecurityGroups, self).get_groups(interfaces) 

1899 

1900 for idx, i in enumerate(interfaces): 

1901 client.modify_network_interface_attribute( 

1902 NetworkInterfaceId=i['NetworkInterfaceId'], 

1903 Groups=groups[idx]) 

1904 

1905 

1906@actions.register('autorecover-alarm') 

1907class AutorecoverAlarm(BaseAction): 

1908 """Adds a cloudwatch metric alarm to recover an EC2 instance. 

1909 

1910 This action takes effect on instances that are NOT part 

1911 of an ASG. 

1912 

1913 :Example: 

1914 

1915 .. code-block:: yaml 

1916 

1917 policies: 

1918 - name: ec2-autorecover-alarm 

1919 resource: ec2 

1920 filters: 

1921 - singleton 

1922 actions: 

1923 - autorecover-alarm 

1924 

1925 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-recover.html 

1926 """ 

1927 

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() 

1934 

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 ) 

1967 

1968 

1969@actions.register('set-instance-profile') 

1970class SetInstanceProfile(BaseAction): 

1971 """Sets (add, modify, remove) the instance profile for a running EC2 instance. 

1972 

1973 :Example: 

1974 

1975 .. code-block:: yaml 

1976 

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 

1985 

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 """ 

1989 

1990 schema = type_schema( 

1991 'set-instance-profile', 

1992 **{'name': {'type': 'string'}}) 

1993 

1994 permissions = ( 

1995 'ec2:AssociateIamInstanceProfile', 

1996 'ec2:DisassociateIamInstanceProfile', 

1997 'iam:PassRole') 

1998 

1999 valid_origin_states = ('running', 'pending', 'stopped', 'stopping') 

2000 

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')] 

2008 

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 = {} 

2020 

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 

2030 

2031 p_assoc_id, p_arn = associations[i['InstanceId']] 

2032 

2033 # Already associated to target profile, skip 

2034 if profile_name and p_arn.endswith('/%s' % profile_name): 

2035 continue 

2036 

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) 

2044 

2045 return instances 

2046 

2047 

2048@actions.register('propagate-spot-tags') 

2049class PropagateSpotTags(BaseAction): 

2050 """Propagate Tags that are set at Spot Request level to EC2 instances. 

2051 

2052 :Example: 

2053 

2054 .. code-block:: yaml 

2055 

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 """ 

2068 

2069 schema = type_schema( 

2070 'propagate-spot-tags', 

2071 **{'only_tags': {'type': 'array', 'items': {'type': 'string'}}}) 

2072 

2073 permissions = ( 

2074 'ec2:DescribeInstances', 

2075 'ec2:DescribeSpotInstanceRequests', 

2076 'ec2:DescribeTags', 

2077 'ec2:CreateTags') 

2078 

2079 MAX_TAG_COUNT = 50 

2080 

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 

2089 

2090 client = utils.local_session( 

2091 self.manager.session_factory).client('ec2') 

2092 

2093 request_instance_map = {} 

2094 for i in instances: 

2095 request_instance_map.setdefault( 

2096 i['SpotInstanceRequestId'], []).append(i) 

2097 

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', []) 

2104 

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 

2113 

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] 

2123 

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']) 

2132 

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 

2139 

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()]) 

2145 

2146 self.log.debug( 

2147 "action:%s tags updated on instances:%r" % ( 

2148 self.__class__.__name__.lower(), 

2149 update_instances)) 

2150 

2151 return update_instances 

2152 

2153 

2154class EC2QueryParser(QueryParser): 

2155 

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',) 

2188 

2189 type_name = "EC2" 

2190 

2191 

2192@filters.register('instance-attribute') 

2193class InstanceAttribute(ValueFilter): 

2194 """EC2 Instance Value Filter on a given instance attribute. 

2195 

2196 Filters EC2 Instances with the given instance attribute 

2197 

2198 :Example: 

2199 

2200 .. code-block:: yaml 

2201 

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 """ 

2211 

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') 

2227 

2228 schema = type_schema( 

2229 'instance-attribute', 

2230 rinherit=ValueFilter.schema, 

2231 attribute={'enum': valid_attrs}, 

2232 required=('attribute',)) 

2233 schema_alias = False 

2234 

2235 def get_permissions(self): 

2236 return ('ec2:DescribeInstanceAttribute',) 

2237 

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])] 

2243 

2244 def get_instance_attribute(self, resources, attribute): 

2245 client = utils.local_session( 

2246 self.manager.session_factory).client('ec2') 

2247 

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]] 

2259 

2260 

2261@resources.register('launch-template-version') 

2262class LaunchTemplate(query.QueryResourceManager): 

2263 

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" 

2276 

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 

2287 

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 

2293 

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 [] 

2308 

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] = [] 

2321 

2322 client = utils.local_session(self.session_factory).client('ec2') 

2323 

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 

2344 

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 

2360 

2361 

2362@resources.register('ec2-reserved') 

2363class ReservedInstance(query.QueryResourceManager): 

2364 

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" 

2375 

2376 

2377@resources.register('ec2-host') 

2378class DedicatedHost(query.QueryResourceManager): 

2379 """Custodian resource for managing EC2 Dedicated Hosts. 

2380 """ 

2381 

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',) 

2393 

2394 

2395@resources.register('ec2-spot-fleet-request') 

2396class SpotFleetRequest(query.QueryResourceManager): 

2397 """Custodian resource for managing EC2 Spot Fleet Requests. 

2398 """ 

2399 

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',) 

2411 

2412 

2413SpotFleetRequest.filter_registry.register('offhour', OffHour) 

2414SpotFleetRequest.filter_registry.register('onhour', OnHour) 

2415 

2416 

2417@SpotFleetRequest.action_registry.register('resize') 

2418class AutoscalingSpotFleetRequest(AutoscalingBase): 

2419 permissions = ( 

2420 'ec2:CreateTags', 

2421 'ec2:ModifySpotFleetRequest', 

2422 ) 

2423 

2424 service_namespace = 'ec2' 

2425 scalable_dimension = 'ec2:spot-fleet-request:TargetCapacity' 

2426 

2427 def get_resource_id(self, resource): 

2428 return 'spot-fleet-request/%s' % resource['SpotFleetRequestId'] 

2429 

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 

2436 

2437 def get_resource_desired(self, resource): 

2438 return int(resource['SpotFleetRequestConfig']['TargetCapacity']) 

2439 

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 ) 

2446 

2447 

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. 

2453 

2454 :example: 

2455 

2456 .. code-block:: yaml 

2457 

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 

2464 

2465 :example: 

2466 

2467 Check for EC2 instances with instance profile roles that have an 

2468 attached policy matching a given list: 

2469 

2470 .. code-block:: yaml 

2471 

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 

2481 

2482 :example: 

2483 

2484 Check for EC2 instances with instance profile roles that have 

2485 attached policy names matching a pattern: 

2486 

2487 .. code-block:: yaml 

2488 

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" 

2496 

2497 Check for EC2 instances with instance profile roles that have 

2498 attached policy ARNs matching a pattern: 

2499 

2500 .. code-block:: yaml 

2501 

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 """ 

2511 

2512 permissions = ( 

2513 'iam:GetInstanceProfile', 

2514 'iam:ListInstanceProfiles', 

2515 'iam:ListAttachedRolePolicies') 

2516 

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} 

2521 

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 

2529 

2530 profile = iam_profiles_mapping.get(instance_profile_arn) 

2531 if not profile: 

2532 continue 

2533 

2534 self.get_managed_policies(client, [profile]) 

2535 

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) 

2540 

2541 return results 

2542 

2543 

2544@resources.register('ec2-capacity-reservation') 

2545class CapacityReservation(query.QueryResourceManager): 

2546 """Custodian resource for managing EC2 Capacity Reservation. 

2547 """ 

2548 

2549 class resource_type(query.TypeInfo): 

2550 name = id = 'CapacityReservationId' 

2551 service = 'ec2' 

2552 enum_spec = ('describe_capacity_reservations', 

2553 'CapacityReservations', None) 

2554 

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',)