Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/c7n/resources/ec2.py: 36%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1080 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 

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