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

1038 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:51 +0000

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

26import c7n.filters.vpc as net_filters 

27 

28from c7n.manager import resources 

29from c7n import query, utils 

30from c7n.tags import coalesce_copy_user_tags 

31from c7n.utils import type_schema, filter_empty, jmespath_search, jmespath_compile 

32 

33from c7n.resources.iam import CheckPermissions, SpecificIamProfileManagedPolicy 

34from c7n.resources.securityhub import PostFinding 

35 

36RE_ERROR_INSTANCE_ID = re.compile("'(?P<instance_id>i-.*?)'") 

37 

38filters = FilterRegistry('ec2.filters') 

39actions = ActionRegistry('ec2.actions') 

40 

41 

42class DescribeEC2(query.DescribeSource): 

43 

44 def get_query_params(self, query_params): 

45 queries = QueryFilter.parse(self.manager.data.get('query', [])) 

46 qf = [] 

47 for q in queries: 

48 qd = q.query() 

49 found = False 

50 for f in qf: 

51 if qd['Name'] == f['Name']: 

52 f['Values'].extend(qd['Values']) 

53 found = True 

54 if not found: 

55 qf.append(qd) 

56 query_params = query_params or {} 

57 query_params['Filters'] = qf 

58 return query_params 

59 

60 def augment(self, resources): 

61 """EC2 API and AWOL Tags 

62 

63 While ec2 api generally returns tags when doing describe_x on for 

64 various resources, it may also silently fail to do so unless a tag 

65 is used as a filter. 

66 

67 See footnote on for official documentation. 

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

69 

70 Apriori we may be using custodian to ensure tags (including 

71 name), so there isn't a good default to ensure that we will 

72 always get tags from describe_x calls. 

73 """ 

74 # First if we're in event based lambda go ahead and skip this, 

75 # tags can't be trusted in ec2 instances immediately post creation. 

76 if not resources or self.manager.data.get( 

77 'mode', {}).get('type', '') in ( 

78 'cloudtrail', 'ec2-instance-state'): 

79 return resources 

80 

81 # AWOL detector, so we don't make extraneous api calls. 

82 resource_count = len(resources) 

83 search_count = min(int(resource_count % 0.05) + 1, 5) 

84 if search_count > resource_count: 

85 search_count = resource_count 

86 found = False 

87 for r in random.sample(resources, search_count): 

88 if 'Tags' in r: 

89 found = True 

90 break 

91 

92 if found: 

93 return resources 

94 

95 # Okay go and do the tag lookup 

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

97 tag_set = self.manager.retry( 

98 client.describe_tags, 

99 Filters=[{'Name': 'resource-type', 

100 'Values': ['instance']}])['Tags'] 

101 resource_tags = {} 

102 for t in tag_set: 

103 t.pop('ResourceType') 

104 rid = t.pop('ResourceId') 

105 resource_tags.setdefault(rid, []).append(t) 

106 

107 m = self.manager.get_model() 

108 for r in resources: 

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

110 return resources 

111 

112 

113@resources.register('ec2') 

114class EC2(query.QueryResourceManager): 

115 

116 class resource_type(query.TypeInfo): 

117 service = 'ec2' 

118 arn_type = 'instance' 

119 enum_spec = ('describe_instances', 'Reservations[].Instances[]', None) 

120 id = 'InstanceId' 

121 filter_name = 'InstanceIds' 

122 filter_type = 'list' 

123 name = 'PublicDnsName' 

124 date = 'LaunchTime' 

125 dimension = 'InstanceId' 

126 cfn_type = config_type = "AWS::EC2::Instance" 

127 id_prefix = 'i-' 

128 

129 default_report_fields = ( 

130 'CustodianDate', 

131 'InstanceId', 

132 'tag:Name', 

133 'InstanceType', 

134 'LaunchTime', 

135 'VpcId', 

136 'PrivateIpAddress', 

137 ) 

138 

139 filter_registry = filters 

140 action_registry = actions 

141 

142 # if we have to do a fallback scenario where tags don't come in describe 

143 permissions = ('ec2:DescribeTags',) 

144 source_mapping = { 

145 'describe': DescribeEC2, 

146 'config': query.ConfigSource 

147 } 

148 

149 

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

151class SecurityGroupFilter(net_filters.SecurityGroupFilter): 

152 

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

154 

155 

156@filters.register('subnet') 

157class SubnetFilter(net_filters.SubnetFilter): 

158 

159 RelatedIdsExpression = "NetworkInterfaces[].SubnetId" 

160 

161 

162@filters.register('vpc') 

163class VpcFilter(net_filters.VpcFilter): 

164 

165 RelatedIdsExpression = "VpcId" 

166 

167 

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

169class ComputePermissions(CheckPermissions): 

170 

171 def get_iam_arns(self, resources): 

172 profile_arn_map = { 

173 r['IamInstanceProfile']['Arn']: r['IamInstanceProfile']['Id'] 

174 for r in resources if 'IamInstanceProfile' in r} 

175 

176 # py2 compat on dict ordering 

177 profile_arns = list(profile_arn_map.items()) 

178 profile_role_map = { 

179 arn: profile['Roles'][0]['Arn'] 

180 for arn, profile in zip( 

181 [p[0] for p in profile_arns], 

182 self.manager.get_resource_manager( 

183 'iam-profile').get_resources( 

184 [p[0].split('/', 1)[-1] for p in profile_arns]))} 

185 return [ 

186 profile_role_map.get(r.get('IamInstanceProfile', {}).get('Arn')) 

187 for r in resources] 

188 

189 

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

191class StateTransitionAge(AgeFilter): 

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

193 

194 .. code-block:: yaml 

195 

196 policies: 

197 - name: ec2-state-running-7-days 

198 resource: ec2 

199 filters: 

200 - type: state-age 

201 op: ge 

202 days: 7 

203 """ 

204 RE_PARSE_AGE = re.compile(r"\(.*?\)") 

205 

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

207 # to pass AgeFilter's validate method 

208 date_attribute = "dummy" 

209 

210 schema = type_schema( 

211 'state-age', 

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

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

214 

215 def get_resource_date(self, i): 

216 v = i.get('StateTransitionReason') 

217 if not v: 

218 return None 

219 dates = self.RE_PARSE_AGE.findall(v) 

220 if dates: 

221 return parse(dates[0][1:-1]) 

222 return None 

223 

224 

225@filters.register('ebs') 

226class AttachedVolume(ValueFilter): 

227 """EC2 instances with EBS backed volume 

228 

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

230 

231 :Example: 

232 

233 .. code-block:: yaml 

234 

235 policies: 

236 - name: ec2-encrypted-ebs-volumes 

237 resource: ec2 

238 filters: 

239 - type: ebs 

240 key: Encrypted 

241 value: true 

242 """ 

243 

244 schema = type_schema( 

245 'ebs', rinherit=ValueFilter.schema, 

246 **{'operator': {'enum': ['and', 'or']}, 

247 'skip-devices': {'type': 'array', 'items': {'type': 'string'}}}) 

248 schema_alias = False 

249 

250 def get_permissions(self): 

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

252 

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

254 self.volume_map = self.get_volume_mapping(resources) 

255 self.skip = self.data.get('skip-devices', []) 

256 self.operator = self.data.get( 

257 'operator', 'or') == 'or' and any or all 

258 return list(filter(self, resources)) 

259 

260 def get_volume_mapping(self, resources): 

261 volume_map = {} 

262 manager = self.manager.get_resource_manager('ebs') 

263 for instance_set in utils.chunks(resources, 200): 

264 volume_ids = [] 

265 for i in instance_set: 

266 for bd in i.get('BlockDeviceMappings', ()): 

267 if 'Ebs' not in bd: 

268 continue 

269 volume_ids.append(bd['Ebs']['VolumeId']) 

270 for v in manager.get_resources(volume_ids): 

271 if not v['Attachments']: 

272 continue 

273 volume_map.setdefault( 

274 v['Attachments'][0]['InstanceId'], []).append(v) 

275 return volume_map 

276 

277 def __call__(self, i): 

278 volumes = self.volume_map.get(i['InstanceId']) 

279 if not volumes: 

280 return False 

281 if self.skip: 

282 for v in list(volumes): 

283 for a in v.get('Attachments', []): 

284 if a['Device'] in self.skip: 

285 volumes.remove(v) 

286 return self.operator(map(self.match, volumes)) 

287 

288 

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

290class DisableApiStop(Filter): 

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

292 

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

294 

295 :Example: 

296 

297 .. code-block:: yaml 

298 

299 policies: 

300 - name: stop-protection-enabled 

301 resource: ec2 

302 filters: 

303 - type: stop-protected 

304 

305 :Example: 

306 

307 .. code-block:: yaml 

308 

309 policies: 

310 - name: stop-protection-NOT-enabled 

311 resource: ec2 

312 filters: 

313 - not: 

314 - type: stop-protected 

315 """ 

316 

317 schema = type_schema('stop-protected') 

318 permissions = ('ec2:DescribeInstanceAttribute',) 

319 

320 def process(self, resources: List[dict], event=None) -> List[dict]: 

321 client = utils.local_session( 

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

323 return [r for r in resources 

324 if self._is_stop_protection_enabled(client, r)] 

325 

326 def _is_stop_protection_enabled(self, client, instance: dict) -> bool: 

327 attr_val = self.manager.retry( 

328 client.describe_instance_attribute, 

329 Attribute='disableApiStop', 

330 InstanceId=instance['InstanceId'] 

331 ) 

332 return attr_val['DisableApiStop']['Value'] 

333 

334 def validate(self) -> None: 

335 botocore_min_version = '1.26.7' 

336 

337 if LooseVersion(botocore.__version__) < LooseVersion(botocore_min_version): 

338 raise PolicyValidationError( 

339 "'stop-protected' filter requires botocore version " 

340 f'{botocore_min_version} or above. ' 

341 f'Installed version is {botocore.__version__}.' 

342 ) 

343 

344 

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

346class DisableApiTermination(Filter): 

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

348 

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

350 

351 :Example: 

352 

353 .. code-block:: yaml 

354 

355 policies: 

356 - name: termination-protection-enabled 

357 resource: ec2 

358 filters: 

359 - type: termination-protected 

360 

361 :Example: 

362 

363 .. code-block:: yaml 

364 

365 policies: 

366 - name: termination-protection-NOT-enabled 

367 resource: ec2 

368 filters: 

369 - not: 

370 - type: termination-protected 

371 """ 

372 

373 schema = type_schema('termination-protected') 

374 permissions = ('ec2:DescribeInstanceAttribute',) 

375 

376 def get_permissions(self): 

377 perms = list(self.permissions) 

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

379 return perms 

380 

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

382 client = utils.local_session( 

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

384 return [r for r in resources 

385 if self.is_termination_protection_enabled(client, r)] 

386 

387 def is_termination_protection_enabled(self, client, inst): 

388 attr_val = self.manager.retry( 

389 client.describe_instance_attribute, 

390 Attribute='disableApiTermination', 

391 InstanceId=inst['InstanceId'] 

392 ) 

393 return attr_val['DisableApiTermination']['Value'] 

394 

395 

396class InstanceImageBase: 

397 

398 def prefetch_instance_images(self, instances): 

399 image_ids = [i['ImageId'] for i in instances if 'c7n:instance-image' not in i] 

400 self.image_map = self.get_local_image_mapping(image_ids) 

401 

402 def get_base_image_mapping(self): 

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

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

405 

406 def get_instance_image(self, instance): 

407 image = instance.get('c7n:instance-image', None) 

408 if not image: 

409 image = instance['c7n:instance-image'] = self.image_map.get(instance['ImageId'], None) 

410 return image 

411 

412 def get_local_image_mapping(self, image_ids): 

413 base_image_map = self.get_base_image_mapping() 

414 resources = {i: base_image_map[i] for i in image_ids if i in base_image_map} 

415 missing = list(set(image_ids) - set(resources.keys())) 

416 if missing: 

417 loaded = self.manager.get_resource_manager('ami').get_resources(missing, False) 

418 resources.update({image['ImageId']: image for image in loaded}) 

419 return resources 

420 

421 

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

423class ImageAge(AgeFilter, InstanceImageBase): 

424 """EC2 AMI age filter 

425 

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

427 

428 :Example: 

429 

430 .. code-block:: yaml 

431 

432 policies: 

433 - name: ec2-ancient-ami 

434 resource: ec2 

435 filters: 

436 - type: image-age 

437 op: ge 

438 days: 90 

439 """ 

440 

441 date_attribute = "CreationDate" 

442 

443 schema = type_schema( 

444 'image-age', 

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

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

447 

448 def get_permissions(self): 

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

450 

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

452 self.prefetch_instance_images(resources) 

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

454 

455 def get_resource_date(self, i): 

456 image = self.get_instance_image(i) 

457 if image: 

458 return parse(image['CreationDate']) 

459 else: 

460 return parse("2000-01-01T01:01:01.000Z") 

461 

462 

463@filters.register('image') 

464class InstanceImage(ValueFilter, InstanceImageBase): 

465 

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

467 schema_alias = False 

468 

469 def get_permissions(self): 

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

471 

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

473 self.prefetch_instance_images(resources) 

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

475 

476 def __call__(self, i): 

477 image = self.get_instance_image(i) 

478 # Finally, if we have no image... 

479 if not image: 

480 self.log.warning( 

481 "Could not locate image for instance:%s ami:%s" % ( 

482 i['InstanceId'], i["ImageId"])) 

483 # Match instead on empty skeleton? 

484 return False 

485 return self.match(image) 

486 

487 

488@filters.register('offhour') 

489class InstanceOffHour(OffHour): 

490 """Custodian OffHour filter 

491 

492 Filters running EC2 instances with the intent to stop at a given hour of 

493 the day. A list of days to excluded can be included as a list of strings 

494 with the format YYYY-MM-DD. Alternatively, the list (using the same syntax) 

495 can be taken from a specified url. 

496 

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

498 `state-filter: false` 

499 

500 :Example: 

501 

502 .. code-block:: yaml 

503 

504 policies: 

505 - name: offhour-evening-stop 

506 resource: ec2 

507 filters: 

508 - type: offhour 

509 tag: custodian_downtime 

510 default_tz: et 

511 offhour: 20 

512 actions: 

513 - stop 

514 

515 - name: offhour-evening-stop-skip-holidays 

516 resource: ec2 

517 filters: 

518 - type: offhour 

519 tag: custodian_downtime 

520 default_tz: et 

521 offhour: 20 

522 skip-days: ['2017-12-25'] 

523 actions: 

524 - stop 

525 

526 - name: offhour-evening-stop-skip-holidays-from 

527 resource: ec2 

528 filters: 

529 - type: offhour 

530 tag: custodian_downtime 

531 default_tz: et 

532 offhour: 20 

533 skip-days-from: 

534 expr: 0 

535 format: csv 

536 url: 's3://location/holidays.csv' 

537 actions: 

538 - stop 

539 """ 

540 

541 schema = type_schema( 

542 'offhour', rinherit=OffHour.schema, 

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

544 schema_alias = False 

545 

546 valid_origin_states = ('running',) 

547 

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

549 if self.data.get('state-filter', True): 

550 return super(InstanceOffHour, self).process( 

551 self.filter_resources(resources, 'State.Name', self.valid_origin_states)) 

552 else: 

553 return super(InstanceOffHour, self).process(resources) 

554 

555 

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

557class EC2NetworkLocation(net_filters.NetworkLocation): 

558 

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

560 'stopped') 

561 

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

563 resources = self.filter_resources(resources, 'State.Name', self.valid_origin_states) 

564 if not resources: 

565 return [] 

566 return super(EC2NetworkLocation, self).process(resources) 

567 

568 

569@filters.register('onhour') 

570class InstanceOnHour(OnHour): 

571 """Custodian OnHour filter 

572 

573 Filters stopped EC2 instances with the intent to start at a given hour of 

574 the day. A list of days to excluded can be included as a list of strings 

575 with the format YYYY-MM-DD. Alternatively, the list (using the same syntax) 

576 can be taken from a specified url. 

577 

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

579 `state-filter: false` 

580 

581 :Example: 

582 

583 .. code-block:: yaml 

584 

585 policies: 

586 - name: onhour-morning-start 

587 resource: ec2 

588 filters: 

589 - type: onhour 

590 tag: custodian_downtime 

591 default_tz: et 

592 onhour: 6 

593 actions: 

594 - start 

595 

596 - name: onhour-morning-start-skip-holidays 

597 resource: ec2 

598 filters: 

599 - type: onhour 

600 tag: custodian_downtime 

601 default_tz: et 

602 onhour: 6 

603 skip-days: ['2017-12-25'] 

604 actions: 

605 - start 

606 

607 - name: onhour-morning-start-skip-holidays-from 

608 resource: ec2 

609 filters: 

610 - type: onhour 

611 tag: custodian_downtime 

612 default_tz: et 

613 onhour: 6 

614 skip-days-from: 

615 expr: 0 

616 format: csv 

617 url: 's3://location/holidays.csv' 

618 actions: 

619 - start 

620 """ 

621 

622 schema = type_schema( 

623 'onhour', rinherit=OnHour.schema, 

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

625 schema_alias = False 

626 

627 valid_origin_states = ('stopped',) 

628 

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

630 if self.data.get('state-filter', True): 

631 return super(InstanceOnHour, self).process( 

632 self.filter_resources(resources, 'State.Name', self.valid_origin_states)) 

633 else: 

634 return super(InstanceOnHour, self).process(resources) 

635 

636 

637@filters.register('ephemeral') 

638class EphemeralInstanceFilter(Filter): 

639 """EC2 instances with ephemeral storage 

640 

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

642 root device) 

643 

644 :Example: 

645 

646 .. code-block:: yaml 

647 

648 policies: 

649 - name: ec2-ephemeral-instances 

650 resource: ec2 

651 filters: 

652 - type: ephemeral 

653 

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

655 """ 

656 

657 schema = type_schema('ephemeral') 

658 

659 def __call__(self, i): 

660 return self.is_ephemeral(i) 

661 

662 @staticmethod 

663 def is_ephemeral(i): 

664 for bd in i.get('BlockDeviceMappings', []): 

665 if bd['DeviceName'] in ('/dev/sda1', '/dev/xvda', 'xvda'): 

666 if 'Ebs' in bd: 

667 return False 

668 return True 

669 return True 

670 

671 

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

673class UpTimeFilter(AgeFilter): 

674 

675 date_attribute = "LaunchTime" 

676 

677 schema = type_schema( 

678 'instance-uptime', 

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

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

681 

682 

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

684class InstanceAgeFilter(AgeFilter): 

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

686 

687 :Example: 

688 

689 .. code-block:: yaml 

690 

691 policies: 

692 - name: ec2-30-days-plus 

693 resource: ec2 

694 filters: 

695 - type: instance-age 

696 op: ge 

697 days: 30 

698 """ 

699 

700 date_attribute = "LaunchTime" 

701 ebs_key_func = operator.itemgetter('AttachTime') 

702 

703 schema = type_schema( 

704 'instance-age', 

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

706 days={'type': 'number'}, 

707 hours={'type': 'number'}, 

708 minutes={'type': 'number'}) 

709 

710 def get_resource_date(self, i): 

711 # LaunchTime is basically how long has the instance 

712 # been on, use the oldest ebs vol attach time 

713 ebs_vols = [ 

714 block['Ebs'] for block in i['BlockDeviceMappings'] 

715 if 'Ebs' in block] 

716 if not ebs_vols: 

717 # Fall back to using age attribute (ephemeral instances) 

718 return super(InstanceAgeFilter, self).get_resource_date(i) 

719 # Lexographical sort on date 

720 ebs_vols = sorted(ebs_vols, key=self.ebs_key_func) 

721 return ebs_vols[0]['AttachTime'] 

722 

723 

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

725class DefaultVpc(net_filters.DefaultVpcBase): 

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

727 """ 

728 

729 schema = type_schema('default-vpc') 

730 

731 def __call__(self, ec2): 

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

733 

734 

735def deserialize_user_data(user_data): 

736 data = base64.b64decode(user_data) 

737 # try raw and compressed 

738 try: 

739 return data.decode('utf8') 

740 except UnicodeDecodeError: 

741 return zlib.decompress(data, 16).decode('utf8') 

742 

743 

744@filters.register('user-data') 

745class UserData(ValueFilter): 

746 """Filter on EC2 instances which have matching userdata. 

747 Note: It is highly recommended to use regexes with the ?sm flags, since Custodian 

748 uses re.match() and userdata spans multiple lines. 

749 

750 :example: 

751 

752 .. code-block:: yaml 

753 

754 policies: 

755 - name: ec2_userdata_stop 

756 resource: ec2 

757 filters: 

758 - type: user-data 

759 op: regex 

760 value: (?smi).*password= 

761 actions: 

762 - stop 

763 """ 

764 

765 schema = type_schema('user-data', rinherit=ValueFilter.schema) 

766 schema_alias = False 

767 batch_size = 50 

768 annotation = 'c7n:user-data' 

769 permissions = ('ec2:DescribeInstanceAttribute',) 

770 

771 def __init__(self, data, manager): 

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

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

774 

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

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

777 results = [] 

778 with self.executor_factory(max_workers=3) as w: 

779 futures = {} 

780 for instance_set in utils.chunks(resources, self.batch_size): 

781 futures[w.submit( 

782 self.process_instance_set, 

783 client, instance_set)] = instance_set 

784 

785 for f in as_completed(futures): 

786 if f.exception(): 

787 self.log.error( 

788 "Error processing userdata on instance set %s", f.exception()) 

789 results.extend(f.result()) 

790 return results 

791 

792 def process_instance_set(self, client, resources): 

793 results = [] 

794 for r in resources: 

795 if self.annotation not in r: 

796 try: 

797 result = client.describe_instance_attribute( 

798 Attribute='userData', 

799 InstanceId=r['InstanceId']) 

800 except ClientError as e: 

801 if e.response['Error']['Code'] == 'InvalidInstanceId.NotFound': 

802 continue 

803 if 'Value' not in result['UserData']: 

804 r[self.annotation] = None 

805 else: 

806 r[self.annotation] = deserialize_user_data( 

807 result['UserData']['Value']) 

808 if self.match(r): 

809 results.append(r) 

810 return results 

811 

812 

813@filters.register('singleton') 

814class SingletonFilter(Filter): 

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

816 

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

818 and do not have Cloudwatch recover alarms. 

819 

820 :Example: 

821 

822 .. code-block:: yaml 

823 

824 policies: 

825 - name: ec2-recover-instances 

826 resource: ec2 

827 filters: 

828 - singleton 

829 actions: 

830 - type: tag 

831 key: problem 

832 value: instance is not resilient 

833 

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

835 """ 

836 

837 schema = type_schema('singleton') 

838 

839 permissions = ('cloudwatch:DescribeAlarmsForMetric',) 

840 

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

842 

843 in_asg = ValueFilter({ 

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

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

846 

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

848 return super(SingletonFilter, self).process( 

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

850 

851 def __call__(self, i): 

852 if self.in_asg(i): 

853 return False 

854 else: 

855 return not self.has_recover_alarm(i) 

856 

857 def has_recover_alarm(self, i): 

858 client = utils.local_session(self.manager.session_factory).client('cloudwatch') 

859 alarms = client.describe_alarms_for_metric( 

860 MetricName='StatusCheckFailed_System', 

861 Namespace='AWS/EC2', 

862 Dimensions=[ 

863 { 

864 'Name': 'InstanceId', 

865 'Value': i['InstanceId'] 

866 } 

867 ] 

868 ) 

869 

870 for i in alarms['MetricAlarms']: 

871 for a in i['AlarmActions']: 

872 if ( 

873 a.startswith('arn:aws:automate:') and 

874 a.endswith(':ec2:recover') 

875 ): 

876 return True 

877 

878 return False 

879 

880 

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

882class SsmStatus(ValueFilter): 

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

884 

885 :Example: 

886 

887 Find ubuntu 18.04 instances are active with ssm. 

888 

889 .. code-block:: yaml 

890 

891 policies: 

892 - name: ec2-ssm-check 

893 resource: ec2 

894 filters: 

895 - type: ssm 

896 key: PingStatus 

897 value: Online 

898 - type: ssm 

899 key: PlatformName 

900 value: Ubuntu 

901 - type: ssm 

902 key: PlatformVersion 

903 value: 18.04 

904 """ 

905 schema = type_schema('ssm', rinherit=ValueFilter.schema) 

906 schema_alias = False 

907 permissions = ('ssm:DescribeInstanceInformation',) 

908 annotation = 'c7n:SsmState' 

909 

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

911 client = utils.local_session(self.manager.session_factory).client('ssm') 

912 results = [] 

913 for resource_set in utils.chunks( 

914 [r for r in resources if self.annotation not in r], 50): 

915 self.process_resource_set(client, resource_set) 

916 for r in resources: 

917 if self.match(r[self.annotation]): 

918 results.append(r) 

919 return results 

920 

921 def process_resource_set(self, client, resources): 

922 instance_ids = [i['InstanceId'] for i in resources] 

923 info_map = { 

924 info['InstanceId']: info for info in 

925 client.describe_instance_information( 

926 Filters=[{'Key': 'InstanceIds', 'Values': instance_ids}]).get( 

927 'InstanceInformationList', [])} 

928 for r in resources: 

929 r[self.annotation] = info_map.get(r['InstanceId'], {}) 

930 

931 

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

933class SsmCompliance(Filter): 

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

935 

936 :Example: 

937 

938 Find non-compliant ec2 instances. 

939 

940 .. code-block:: yaml 

941 

942 policies: 

943 - name: ec2-ssm-compliance 

944 resource: ec2 

945 filters: 

946 - type: ssm-compliance 

947 compliance_types: 

948 - Association 

949 - Patch 

950 severity: 

951 - CRITICAL 

952 - HIGH 

953 - MEDIUM 

954 - LOW 

955 - UNSPECIFIED 

956 states: 

957 - NON_COMPLIANT 

958 eval_filters: 

959 - type: value 

960 key: ExecutionSummary.ExecutionTime 

961 value_type: age 

962 value: 30 

963 op: less-than 

964 """ 

965 schema = type_schema( 

966 'ssm-compliance', 

967 **{'required': ['compliance_types'], 

968 'compliance_types': {'type': 'array', 'items': {'type': 'string'}}, 

969 'severity': {'type': 'array', 'items': {'type': 'string'}}, 

970 'op': {'enum': ['or', 'and']}, 

971 'eval_filters': {'type': 'array', 'items': { 

972 'oneOf': [ 

973 {'$ref': '#/definitions/filters/valuekv'}, 

974 {'$ref': '#/definitions/filters/value'}]}}, 

975 'states': {'type': 'array', 

976 'default': ['NON_COMPLIANT'], 

977 'items': { 

978 'enum': [ 

979 'COMPLIANT', 

980 'NON_COMPLIANT' 

981 ]}}}) 

982 permissions = ('ssm:ListResourceComplianceSummaries',) 

983 annotation = 'c7n:ssm-compliance' 

984 

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

986 op = self.data.get('op', 'or') == 'or' and any or all 

987 eval_filters = [] 

988 for f in self.data.get('eval_filters', ()): 

989 vf = ValueFilter(f) 

990 vf.annotate = False 

991 eval_filters.append(vf) 

992 

993 client = utils.local_session(self.manager.session_factory).client('ssm') 

994 filters = [ 

995 { 

996 'Key': 'Status', 

997 'Values': self.data['states'], 

998 'Type': 'EQUAL' 

999 }, 

1000 { 

1001 'Key': 'ComplianceType', 

1002 'Values': self.data['compliance_types'], 

1003 'Type': 'EQUAL' 

1004 } 

1005 ] 

1006 severity = self.data.get('severity') 

1007 if severity: 

1008 filters.append( 

1009 { 

1010 'Key': 'OverallSeverity', 

1011 'Values': severity, 

1012 'Type': 'EQUAL' 

1013 }) 

1014 

1015 resource_map = {} 

1016 pager = client.get_paginator('list_resource_compliance_summaries') 

1017 for page in pager.paginate(Filters=filters): 

1018 items = page['ResourceComplianceSummaryItems'] 

1019 for i in items: 

1020 if not eval_filters: 

1021 resource_map.setdefault( 

1022 i['ResourceId'], []).append(i) 

1023 continue 

1024 if op([f.match(i) for f in eval_filters]): 

1025 resource_map.setdefault( 

1026 i['ResourceId'], []).append(i) 

1027 

1028 results = [] 

1029 for r in resources: 

1030 result = resource_map.get(r['InstanceId']) 

1031 if result: 

1032 r[self.annotation] = result 

1033 results.append(r) 

1034 

1035 return results 

1036 

1037 

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

1039class MonitorInstances(BaseAction): 

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

1041 

1042 The different states of detailed monitoring status are : 

1043 'disabled'|'disabling'|'enabled'|'pending' 

1044 (https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.describe_instances) 

1045 

1046 :Example: 

1047 

1048 .. code-block:: yaml 

1049 

1050 policies: 

1051 - name: ec2-detailed-monitoring-activation 

1052 resource: ec2 

1053 filters: 

1054 - Monitoring.State: disabled 

1055 actions: 

1056 - type: set-monitoring 

1057 state: enable 

1058 

1059 References 

1060 

1061 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-cloudwatch-new.html 

1062 """ 

1063 schema = type_schema('set-monitoring', 

1064 **{'state': {'enum': ['enable', 'disable']}}) 

1065 permissions = ('ec2:MonitorInstances', 'ec2:UnmonitorInstances') 

1066 

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

1068 client = utils.local_session( 

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

1070 actions = { 

1071 'enable': self.enable_monitoring, 

1072 'disable': self.disable_monitoring 

1073 } 

1074 for instances_set in utils.chunks(resources, 20): 

1075 actions[self.data.get('state')](client, instances_set) 

1076 

1077 def enable_monitoring(self, client, resources): 

1078 try: 

1079 client.monitor_instances( 

1080 InstanceIds=[inst['InstanceId'] for inst in resources] 

1081 ) 

1082 except ClientError as e: 

1083 if e.response['Error']['Code'] != 'InvalidInstanceId.NotFound': 

1084 raise 

1085 

1086 def disable_monitoring(self, client, resources): 

1087 try: 

1088 client.unmonitor_instances( 

1089 InstanceIds=[inst['InstanceId'] for inst in resources] 

1090 ) 

1091 except ClientError as e: 

1092 if e.response['Error']['Code'] != 'InvalidInstanceId.NotFound': 

1093 raise 

1094 

1095 

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

1097class SetMetadataServerAccess(BaseAction): 

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

1099 

1100 :example: 

1101 

1102 Require instances to use IMDSv2 

1103 

1104 .. code-block:: yaml 

1105 

1106 policies: 

1107 - name: ec2-require-imdsv2 

1108 resource: ec2 

1109 filters: 

1110 - MetadataOptions.HttpTokens: optional 

1111 actions: 

1112 - type: set-metadata-access 

1113 tokens: required 

1114 

1115 :example: 

1116 

1117 Disable metadata server access 

1118 

1119 .. code-block: yaml 

1120 

1121 policies: 

1122 - name: ec2-disable-imds 

1123 resource: ec2 

1124 filters: 

1125 - MetadataOptions.HttpEndpoint: enabled 

1126 actions: 

1127 - type: set-metadata-access 

1128 endpoint: disabled 

1129 

1130 policies: 

1131 - name: ec2-enable-metadata-tags 

1132 resource: ec2 

1133 filters: 

1134 - MetadataOptions.InstanceMetadataTags: disabled 

1135 actions: 

1136 - type: set-metadata-access 

1137 metadata-tags: enabled 

1138 

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

1140 """ 

1141 

1142 AllowedValues = { 

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

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

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

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

1147 } 

1148 

1149 schema = type_schema( 

1150 'set-metadata-access', 

1151 anyOf=[{'required': ['endpoint']}, 

1152 {'required': ['tokens']}, 

1153 {'required': ['metadatatags']}, 

1154 {'required': ['hop-limit']}], 

1155 **{'endpoint': {'enum': AllowedValues['HttpEndpoint']}, 

1156 'tokens': {'enum': AllowedValues['HttpTokens']}, 

1157 'metadata-tags': {'enum': AllowedValues['InstanceMetadataTags']}, 

1158 'hop-limit': {'type': 'integer', 'minimum': 1, 'maximum': 64}} 

1159 ) 

1160 permissions = ('ec2:ModifyInstanceMetadataOptions',) 

1161 

1162 def get_params(self): 

1163 return filter_empty({ 

1164 'HttpEndpoint': self.data.get('endpoint'), 

1165 'HttpTokens': self.data.get('tokens'), 

1166 'InstanceMetadataTags': self.data.get('metadata-tags'), 

1167 'HttpPutResponseHopLimit': self.data.get('hop-limit')}) 

1168 

1169 def process(self, resources): 

1170 params = self.get_params() 

1171 for k, v in params.items(): 

1172 allowed_values = list(self.AllowedValues[k]) 

1173 allowed_values.remove(v) 

1174 resources = self.filter_resources( 

1175 resources, 'MetadataOptions.%s' % k, allowed_values) 

1176 

1177 if not resources: 

1178 return 

1179 

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

1181 for r in resources: 

1182 self.manager.retry( 

1183 client.modify_instance_metadata_options, 

1184 ignore_err_codes=('InvalidInstanceId.NotFound',), 

1185 InstanceId=r['InstanceId'], 

1186 **params) 

1187 

1188 

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

1190class InstanceFinding(PostFinding): 

1191 

1192 resource_type = 'AwsEc2Instance' 

1193 

1194 def format_resource(self, r): 

1195 ip_addresses = jmespath_search( 

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

1197 

1198 # limit to max 10 ip addresses, per security hub service limits 

1199 ip_addresses = ip_addresses and ip_addresses[:10] or ip_addresses 

1200 details = { 

1201 "Type": r["InstanceType"], 

1202 "ImageId": r["ImageId"], 

1203 "IpV4Addresses": ip_addresses, 

1204 "KeyName": r.get("KeyName"), 

1205 "LaunchedAt": r["LaunchTime"].isoformat() 

1206 } 

1207 

1208 if "VpcId" in r: 

1209 details["VpcId"] = r["VpcId"] 

1210 if "SubnetId" in r: 

1211 details["SubnetId"] = r["SubnetId"] 

1212 # config will use an empty key 

1213 if "IamInstanceProfile" in r and r['IamInstanceProfile']: 

1214 details["IamInstanceProfileArn"] = r["IamInstanceProfile"]["Arn"] 

1215 

1216 instance = { 

1217 "Type": self.resource_type, 

1218 "Id": "arn:{}:ec2:{}:{}:instance/{}".format( 

1219 utils.REGION_PARTITION_MAP.get(self.manager.config.region, 'aws'), 

1220 self.manager.config.region, 

1221 self.manager.config.account_id, 

1222 r["InstanceId"]), 

1223 "Region": self.manager.config.region, 

1224 "Tags": {t["Key"]: t["Value"] for t in r.get("Tags", [])}, 

1225 "Details": {self.resource_type: filter_empty(details)}, 

1226 } 

1227 

1228 instance = filter_empty(instance) 

1229 return instance 

1230 

1231 

1232@actions.register('start') 

1233class Start(BaseAction): 

1234 """Starts a previously stopped EC2 instance. 

1235 

1236 :Example: 

1237 

1238 .. code-block:: yaml 

1239 

1240 policies: 

1241 - name: ec2-start-stopped-instances 

1242 resource: ec2 

1243 query: 

1244 - instance-state-name: stopped 

1245 actions: 

1246 - start 

1247 

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

1249 """ 

1250 

1251 valid_origin_states = ('stopped',) 

1252 schema = type_schema('start') 

1253 permissions = ('ec2:StartInstances',) 

1254 batch_size = 10 

1255 exception = None 

1256 

1257 def _filter_ec2_with_volumes(self, instances): 

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

1259 

1260 def process(self, instances): 

1261 instances = self._filter_ec2_with_volumes( 

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

1263 if not len(instances): 

1264 return 

1265 

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

1267 failures = {} 

1268 

1269 # Play nice around aws having insufficient capacity... 

1270 for itype, t_instances in utils.group_by( 

1271 instances, 'InstanceType').items(): 

1272 for izone, z_instances in utils.group_by( 

1273 t_instances, 'Placement.AvailabilityZone').items(): 

1274 for batch in utils.chunks(z_instances, self.batch_size): 

1275 fails = self.process_instance_set(client, batch, itype, izone) 

1276 if fails: 

1277 failures["%s %s" % (itype, izone)] = [i['InstanceId'] for i in batch] 

1278 

1279 if failures: 

1280 fail_count = sum(map(len, failures.values())) 

1281 msg = "Could not start %d of %d instances %s" % ( 

1282 fail_count, len(instances), utils.dumps(failures)) 

1283 self.log.warning(msg) 

1284 raise RuntimeError(msg) 

1285 

1286 def process_instance_set(self, client, instances, itype, izone): 

1287 # Setup retry with insufficient capacity as well 

1288 retryable = ('InsufficientInstanceCapacity', 'RequestLimitExceeded', 

1289 'Client.RequestLimitExceeded', 'Server.InsufficientInstanceCapacity'), 

1290 retry = utils.get_retry(retryable, max_attempts=5) 

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

1292 while instance_ids: 

1293 try: 

1294 retry(client.start_instances, InstanceIds=instance_ids) 

1295 break 

1296 except ClientError as e: 

1297 if e.response['Error']['Code'] in retryable: 

1298 # we maxed out on our retries 

1299 return True 

1300 elif e.response['Error']['Code'] == 'IncorrectInstanceState': 

1301 instance_ids.remove(extract_instance_id(e)) 

1302 else: 

1303 raise 

1304 

1305 

1306def extract_instance_id(state_error): 

1307 "Extract an instance id from an error" 

1308 instance_id = None 

1309 match = RE_ERROR_INSTANCE_ID.search(str(state_error)) 

1310 if match: 

1311 instance_id = match.groupdict().get('instance_id') 

1312 if match is None or instance_id is None: 

1313 raise ValueError("Could not extract instance id from error: %s" % state_error) 

1314 return instance_id 

1315 

1316 

1317@actions.register('resize') 

1318class Resize(BaseAction): 

1319 """Change an instance's size. 

1320 

1321 An instance can only be resized when its stopped, this action 

1322 can optionally restart an instance if needed to effect the instance 

1323 type change. Instances are always left in the run state they were 

1324 found in. 

1325 

1326 There are a few caveats to be aware of, instance resizing 

1327 needs to maintain compatibility for architecture, virtualization type 

1328 hvm/pv, and ebs optimization at minimum. 

1329 

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

1331 """ 

1332 

1333 schema = type_schema( 

1334 'resize', 

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

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

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

1338 

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

1340 

1341 def get_permissions(self): 

1342 perms = ('ec2:DescribeInstances', 'ec2:ModifyInstanceAttribute') 

1343 if self.data.get('restart', False): 

1344 perms += ('ec2:StopInstances', 'ec2:StartInstances') 

1345 return perms 

1346 

1347 def process(self, resources): 

1348 stopped_instances = self.filter_resources(resources, 'State.Name', ('stopped',)) 

1349 running_instances = self.filter_resources(resources, 'State.Name', ('running',)) 

1350 

1351 if self.data.get('restart') and running_instances: 

1352 Stop({'terminate-ephemeral': False}, 

1353 self.manager).process(running_instances) 

1354 client = utils.local_session( 

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

1356 waiter = client.get_waiter('instance_stopped') 

1357 try: 

1358 waiter.wait( 

1359 InstanceIds=[r['InstanceId'] for r in running_instances]) 

1360 except ClientError as e: 

1361 self.log.exception( 

1362 "Exception stopping instances for resize:\n %s" % e) 

1363 

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

1365 stopped_instances, running_instances), 20): 

1366 self.process_resource_set(instance_set) 

1367 

1368 if self.data.get('restart') and running_instances: 

1369 client.start_instances( 

1370 InstanceIds=[i['InstanceId'] for i in running_instances]) 

1371 return list(itertools.chain(stopped_instances, running_instances)) 

1372 

1373 def process_resource_set(self, instance_set): 

1374 type_map = self.data.get('type-map') 

1375 default_type = self.data.get('default') 

1376 

1377 client = utils.local_session( 

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

1379 

1380 for i in instance_set: 

1381 self.log.debug( 

1382 "resizing %s %s" % (i['InstanceId'], i['InstanceType'])) 

1383 new_type = type_map.get(i['InstanceType'], default_type) 

1384 if new_type == i['InstanceType']: 

1385 continue 

1386 try: 

1387 client.modify_instance_attribute( 

1388 InstanceId=i['InstanceId'], 

1389 InstanceType={'Value': new_type}) 

1390 except ClientError as e: 

1391 self.log.exception( 

1392 "Exception resizing instance:%s new:%s old:%s \n %s" % ( 

1393 i['InstanceId'], new_type, i['InstanceType'], e)) 

1394 

1395 

1396@actions.register('stop') 

1397class Stop(BaseAction): 

1398 """Stops or hibernates a running EC2 instances 

1399 

1400 :Example: 

1401 

1402 .. code-block:: yaml 

1403 

1404 policies: 

1405 - name: ec2-stop-running-instances 

1406 resource: ec2 

1407 query: 

1408 - instance-state-name: running 

1409 actions: 

1410 - stop 

1411 

1412 - name: ec2-hibernate-instances 

1413 resources: ec2 

1414 query: 

1415 - instance-state-name: running 

1416 actions: 

1417 - type: stop 

1418 hibernate: true 

1419 

1420 

1421 Note when using hiberate, instances not configured for hiberation 

1422 will just be stopped. 

1423 """ 

1424 valid_origin_states = ('running',) 

1425 

1426 schema = type_schema( 

1427 'stop', 

1428 **{ 

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

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

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

1432 }, 

1433 ) 

1434 

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

1436 

1437 def get_permissions(self): 

1438 perms = ('ec2:StopInstances',) 

1439 if self.data.get('terminate-ephemeral', False): 

1440 perms += ('ec2:TerminateInstances',) 

1441 if self.data.get("force"): 

1442 perms += ("ec2:ModifyInstanceAttribute",) 

1443 return perms 

1444 

1445 def split_on_storage(self, instances): 

1446 ephemeral = [] 

1447 persistent = [] 

1448 for i in instances: 

1449 if EphemeralInstanceFilter.is_ephemeral(i): 

1450 ephemeral.append(i) 

1451 else: 

1452 persistent.append(i) 

1453 return ephemeral, persistent 

1454 

1455 def split_on_hibernate(self, instances): 

1456 enabled, disabled = [], [] 

1457 for status, i in zip(self.has_hibernate.search(instances), instances): 

1458 if status is True: 

1459 enabled.append(i) 

1460 else: 

1461 disabled.append(i) 

1462 return enabled, disabled 

1463 

1464 def process(self, instances): 

1465 instances = self.filter_resources(instances, 'State.Name', self.valid_origin_states) 

1466 if not len(instances): 

1467 return 

1468 client = utils.local_session( 

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

1470 # Ephemeral instance can't be stopped. 

1471 ephemeral, persistent = self.split_on_storage(instances) 

1472 if self.data.get('terminate-ephemeral', False) and ephemeral: 

1473 self._run_instances_op(client, 'terminate', ephemeral) 

1474 if persistent: 

1475 if self.data.get('hibernate', False): 

1476 enabled, persistent = self.split_on_hibernate(persistent) 

1477 if enabled: 

1478 self._run_instances_op(client, 'stop', enabled, Hibernate=True) 

1479 self._run_instances_op(client, 'stop', persistent) 

1480 return instances 

1481 

1482 def disable_protection(self, client, op, instances): 

1483 def modify_instance(i, attribute): 

1484 try: 

1485 self.manager.retry( 

1486 client.modify_instance_attribute, 

1487 InstanceId=i['InstanceId'], 

1488 Attribute=attribute, 

1489 Value='false', 

1490 ) 

1491 except ClientError as e: 

1492 if e.response['Error']['Code'] == 'IncorrectInstanceState': 

1493 return 

1494 raise 

1495 

1496 def process_instance(i, op): 

1497 modify_instance(i, 'disableApiStop') 

1498 if op == 'terminate': 

1499 modify_instance(i, 'disableApiTermination') 

1500 

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

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

1503 

1504 def _run_instances_op(self, client, op, instances, **kwargs): 

1505 client_op = client.stop_instances 

1506 if op == 'terminate': 

1507 client_op = client.terminate_instances 

1508 

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

1510 

1511 while instances: 

1512 try: 

1513 return self.manager.retry(client_op, InstanceIds=instance_ids, **kwargs) 

1514 except ClientError as e: 

1515 if e.response['Error']['Code'] == 'IncorrectInstanceState': 

1516 instance_ids.remove(extract_instance_id(e)) 

1517 if ( 

1518 e.response['Error']['Code'] == 'OperationNotPermitted' and 

1519 self.data.get('force') 

1520 ): 

1521 self.log.info("Disabling stop and termination protection on instances") 

1522 self.disable_protection( 

1523 client, 

1524 op, 

1525 [i for i in instances if i.get('InstanceLifecycle') != 'spot'], 

1526 ) 

1527 continue 

1528 raise 

1529 

1530 

1531@actions.register('reboot') 

1532class Reboot(BaseAction): 

1533 """Reboots a previously running EC2 instance. 

1534 

1535 :Example: 

1536 

1537 .. code-block:: yaml 

1538 

1539 policies: 

1540 - name: ec2-reboot-instances 

1541 resource: ec2 

1542 query: 

1543 - instance-state-name: running 

1544 actions: 

1545 - reboot 

1546 

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

1548 """ 

1549 

1550 valid_origin_states = ('running',) 

1551 schema = type_schema('reboot') 

1552 permissions = ('ec2:RebootInstances',) 

1553 batch_size = 10 

1554 exception = None 

1555 

1556 def _filter_ec2_with_volumes(self, instances): 

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

1558 

1559 def process(self, instances): 

1560 instances = self._filter_ec2_with_volumes( 

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

1562 if not len(instances): 

1563 return 

1564 

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

1566 failures = {} 

1567 

1568 for batch in utils.chunks(instances, self.batch_size): 

1569 fails = self.process_instance_set(client, batch) 

1570 if fails: 

1571 failures = [i['InstanceId'] for i in batch] 

1572 

1573 if failures: 

1574 fail_count = sum(map(len, failures.values())) 

1575 msg = "Could not reboot %d of %d instances %s" % ( 

1576 fail_count, len(instances), 

1577 utils.dumps(failures)) 

1578 self.log.warning(msg) 

1579 raise RuntimeError(msg) 

1580 

1581 def process_instance_set(self, client, instances): 

1582 # Setup retry with insufficient capacity as well 

1583 retryable = ('InsufficientInstanceCapacity', 'RequestLimitExceeded', 

1584 'Client.RequestLimitExceeded'), 

1585 retry = utils.get_retry(retryable, max_attempts=5) 

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

1587 try: 

1588 retry(client.reboot_instances, InstanceIds=instance_ids) 

1589 except ClientError as e: 

1590 if e.response['Error']['Code'] in retryable: 

1591 return True 

1592 raise 

1593 

1594 

1595@actions.register('terminate') 

1596class Terminate(BaseAction): 

1597 """ Terminate a set of instances. 

1598 

1599 While ec2 offers a bulk delete api, any given instance can be configured 

1600 with api deletion termination protection, so we can't use the bulk call 

1601 reliabily, we need to process the instances individually. Additionally 

1602 If we're configured with 'force' then we'll turn off instance termination 

1603 and stop protection. 

1604 

1605 :Example: 

1606 

1607 .. code-block:: yaml 

1608 

1609 policies: 

1610 - name: ec2-process-termination 

1611 resource: ec2 

1612 filters: 

1613 - type: marked-for-op 

1614 op: terminate 

1615 actions: 

1616 - terminate 

1617 """ 

1618 

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

1620 

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

1622 

1623 def get_permissions(self): 

1624 permissions = ("ec2:TerminateInstances",) 

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

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

1627 return permissions 

1628 

1629 def process_terminate(self, instances): 

1630 client = utils.local_session( 

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

1632 try: 

1633 self.manager.retry( 

1634 client.terminate_instances, 

1635 InstanceIds=[i['InstanceId'] for i in instances]) 

1636 return 

1637 except ClientError as e: 

1638 if e.response['Error']['Code'] != 'OperationNotPermitted': 

1639 raise 

1640 if not self.data.get('force'): 

1641 raise 

1642 

1643 self.log.info("Disabling stop and termination protection on instances") 

1644 self.disable_deletion_protection( 

1645 client, 

1646 [i for i in instances if i.get('InstanceLifecycle') != 'spot']) 

1647 self.manager.retry( 

1648 client.terminate_instances, 

1649 InstanceIds=[i['InstanceId'] for i in instances]) 

1650 

1651 def process(self, instances): 

1652 instances = self.filter_resources(instances, 'State.Name', self.valid_origin_states) 

1653 if not len(instances): 

1654 return 

1655 # limit batch sizes to avoid api limits 

1656 for batch in utils.chunks(instances, 100): 

1657 self.process_terminate(batch) 

1658 

1659 def disable_deletion_protection(self, client, instances): 

1660 

1661 def modify_instance(i, attribute): 

1662 try: 

1663 self.manager.retry( 

1664 client.modify_instance_attribute, 

1665 InstanceId=i['InstanceId'], 

1666 Attribute=attribute, 

1667 Value='false') 

1668 except ClientError as e: 

1669 if e.response['Error']['Code'] == 'IncorrectInstanceState': 

1670 return 

1671 raise 

1672 

1673 def process_instance(i): 

1674 modify_instance(i, 'disableApiTermination') 

1675 modify_instance(i, 'disableApiStop') 

1676 

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

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

1679 

1680 

1681@actions.register('snapshot') 

1682class Snapshot(BaseAction): 

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

1684 

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

1686 

1687 - `copy-volume-tags` copies all the tags from the specified 

1688 volume to the corresponding snapshot. 

1689 - `copy-tags` copies the listed tags from each volume 

1690 to the snapshot. This is mutually exclusive with 

1691 `copy-volume-tags`. 

1692 - `tags` allows new tags to be added to each snapshot when using 

1693 'copy-tags`. If no tags are specified, then the tag 

1694 `custodian_snapshot` is added. 

1695 

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

1697 

1698 :Example: 

1699 

1700 .. code-block:: yaml 

1701 

1702 policies: 

1703 - name: ec2-snapshots 

1704 resource: ec2 

1705 actions: 

1706 - type: snapshot 

1707 copy-tags: 

1708 - Name 

1709 tags: 

1710 custodian_snapshot: True 

1711 """ 

1712 

1713 schema = type_schema( 

1714 'snapshot', 

1715 **{'copy-tags': {'type': 'array', 'items': {'type': 'string'}}, 

1716 'copy-volume-tags': {'type': 'boolean'}, 

1717 'tags': {'type': 'object'}, 

1718 'exclude-boot': {'type': 'boolean', 'default': False}}) 

1719 permissions = ('ec2:CreateSnapshot', 'ec2:CreateTags',) 

1720 

1721 def validate(self): 

1722 if self.data.get('copy-tags') and 'copy-volume-tags' in self.data: 

1723 raise PolicyValidationError( 

1724 "Can specify copy-tags or copy-volume-tags, not both") 

1725 

1726 def process(self, resources): 

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

1728 err = None 

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

1730 futures = {} 

1731 for resource in resources: 

1732 futures[w.submit( 

1733 self.process_volume_set, client, resource)] = resource 

1734 for f in as_completed(futures): 

1735 if f.exception(): 

1736 err = f.exception() 

1737 resource = futures[f] 

1738 self.log.error( 

1739 "Exception creating snapshot set instance:%s \n %s" % ( 

1740 resource['InstanceId'], err)) 

1741 if err: 

1742 raise err 

1743 

1744 def process_volume_set(self, client, resource): 

1745 params = dict( 

1746 InstanceSpecification={ 

1747 'ExcludeBootVolume': self.data.get('exclude-boot', False), 

1748 'InstanceId': resource['InstanceId']}) 

1749 if 'copy-tags' in self.data: 

1750 params['TagSpecifications'] = [{ 

1751 'ResourceType': 'snapshot', 

1752 'Tags': self.get_snapshot_tags(resource)}] 

1753 elif self.data.get('copy-volume-tags', True): 

1754 params['CopyTagsFromSource'] = 'volume' 

1755 

1756 try: 

1757 result = self.manager.retry(client.create_snapshots, **params) 

1758 resource['c7n:snapshots'] = [ 

1759 s['SnapshotId'] for s in result['Snapshots']] 

1760 except ClientError as e: 

1761 err_code = e.response['Error']['Code'] 

1762 if err_code not in ( 

1763 'InvalidInstanceId.NotFound', 

1764 'ConcurrentSnapshotLimitExceeded', 

1765 'IncorrectState'): 

1766 raise 

1767 self.log.warning( 

1768 "action:snapshot instance:%s error:%s", 

1769 resource['InstanceId'], err_code) 

1770 

1771 def get_snapshot_tags(self, resource): 

1772 user_tags = self.data.get('tags', {}) or {'custodian_snapshot': ''} 

1773 copy_tags = self.data.get('copy-tags', []) 

1774 return coalesce_copy_user_tags(resource, copy_tags, user_tags) 

1775 

1776 

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

1778class EC2ModifyVpcSecurityGroups(ModifyVpcSecurityGroupsAction): 

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

1780 

1781 permissions = ("ec2:ModifyNetworkInterfaceAttribute",) 

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

1783 

1784 def process(self, instances): 

1785 if not len(instances): 

1786 return 

1787 client = utils.local_session( 

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

1789 

1790 # handle multiple ENIs 

1791 interfaces = [] 

1792 for i in instances: 

1793 for eni in i['NetworkInterfaces']: 

1794 if i.get('c7n:matched-security-groups'): 

1795 eni['c7n:matched-security-groups'] = i[ 

1796 'c7n:matched-security-groups'] 

1797 if i.get('c7n:NetworkLocation'): 

1798 eni['c7n:NetworkLocation'] = i[ 

1799 'c7n:NetworkLocation'] 

1800 interfaces.append(eni) 

1801 

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

1803 

1804 for idx, i in enumerate(interfaces): 

1805 client.modify_network_interface_attribute( 

1806 NetworkInterfaceId=i['NetworkInterfaceId'], 

1807 Groups=groups[idx]) 

1808 

1809 

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

1811class AutorecoverAlarm(BaseAction): 

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

1813 

1814 This action takes effect on instances that are NOT part 

1815 of an ASG. 

1816 

1817 :Example: 

1818 

1819 .. code-block:: yaml 

1820 

1821 policies: 

1822 - name: ec2-autorecover-alarm 

1823 resource: ec2 

1824 filters: 

1825 - singleton 

1826 actions: 

1827 - autorecover-alarm 

1828 

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

1830 """ 

1831 

1832 schema = type_schema('autorecover-alarm') 

1833 permissions = ('cloudwatch:PutMetricAlarm',) 

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

1835 filter_asg_membership = ValueFilter({ 

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

1837 'value': 'empty'}).validate() 

1838 

1839 def process(self, instances): 

1840 instances = self.filter_asg_membership.process( 

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

1842 if not len(instances): 

1843 return 

1844 client = utils.local_session( 

1845 self.manager.session_factory).client('cloudwatch') 

1846 for i in instances: 

1847 client.put_metric_alarm( 

1848 AlarmName='recover-{}'.format(i['InstanceId']), 

1849 AlarmDescription='Auto Recover {}'.format(i['InstanceId']), 

1850 ActionsEnabled=True, 

1851 AlarmActions=[ 

1852 'arn:{}:automate:{}:ec2:recover'.format( 

1853 utils.REGION_PARTITION_MAP.get( 

1854 self.manager.config.region, 'aws'), 

1855 i['Placement']['AvailabilityZone'][:-1]) 

1856 ], 

1857 MetricName='StatusCheckFailed_System', 

1858 Namespace='AWS/EC2', 

1859 Statistic='Minimum', 

1860 Dimensions=[ 

1861 { 

1862 'Name': 'InstanceId', 

1863 'Value': i['InstanceId'] 

1864 } 

1865 ], 

1866 Period=60, 

1867 EvaluationPeriods=2, 

1868 Threshold=0, 

1869 ComparisonOperator='GreaterThanThreshold' 

1870 ) 

1871 

1872 

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

1874class SetInstanceProfile(BaseAction): 

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

1876 

1877 :Example: 

1878 

1879 .. code-block:: yaml 

1880 

1881 policies: 

1882 - name: set-default-instance-profile 

1883 resource: ec2 

1884 filters: 

1885 - IamInstanceProfile: absent 

1886 actions: 

1887 - type: set-instance-profile 

1888 name: default 

1889 

1890 https://docs.aws.amazon.com/cli/latest/reference/ec2/associate-iam-instance-profile.html 

1891 https://docs.aws.amazon.com/cli/latest/reference/ec2/disassociate-iam-instance-profile.html 

1892 """ 

1893 

1894 schema = type_schema( 

1895 'set-instance-profile', 

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

1897 

1898 permissions = ( 

1899 'ec2:AssociateIamInstanceProfile', 

1900 'ec2:DisassociateIamInstanceProfile', 

1901 'iam:PassRole') 

1902 

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

1904 

1905 def process(self, instances): 

1906 instances = self.filter_resources(instances, 'State.Name', self.valid_origin_states) 

1907 if not len(instances): 

1908 return 

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

1910 profile_name = self.data.get('name') 

1911 profile_instances = [i for i in instances if i.get('IamInstanceProfile')] 

1912 

1913 if profile_instances: 

1914 associations = { 

1915 a['InstanceId']: (a['AssociationId'], a['IamInstanceProfile']['Arn']) 

1916 for a in client.describe_iam_instance_profile_associations( 

1917 Filters=[ 

1918 {'Name': 'instance-id', 

1919 'Values': [i['InstanceId'] for i in profile_instances]}, 

1920 {'Name': 'state', 'Values': ['associating', 'associated']}] 

1921 ).get('IamInstanceProfileAssociations', ())} 

1922 else: 

1923 associations = {} 

1924 

1925 for i in instances: 

1926 if profile_name and i['InstanceId'] not in associations: 

1927 client.associate_iam_instance_profile( 

1928 IamInstanceProfile={'Name': profile_name}, 

1929 InstanceId=i['InstanceId']) 

1930 continue 

1931 # Removing profile and no profile on instance. 

1932 elif profile_name is None and i['InstanceId'] not in associations: 

1933 continue 

1934 

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

1936 

1937 # Already associated to target profile, skip 

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

1939 continue 

1940 

1941 if profile_name is None: 

1942 client.disassociate_iam_instance_profile( 

1943 AssociationId=p_assoc_id) 

1944 else: 

1945 client.replace_iam_instance_profile_association( 

1946 IamInstanceProfile={'Name': profile_name}, 

1947 AssociationId=p_assoc_id) 

1948 

1949 return instances 

1950 

1951 

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

1953class PropagateSpotTags(BaseAction): 

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

1955 

1956 :Example: 

1957 

1958 .. code-block:: yaml 

1959 

1960 policies: 

1961 - name: ec2-spot-instances 

1962 resource: ec2 

1963 filters: 

1964 - State.Name: pending 

1965 - instanceLifecycle: spot 

1966 actions: 

1967 - type: propagate-spot-tags 

1968 only_tags: 

1969 - Name 

1970 - BillingTag 

1971 """ 

1972 

1973 schema = type_schema( 

1974 'propagate-spot-tags', 

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

1976 

1977 permissions = ( 

1978 'ec2:DescribeInstances', 

1979 'ec2:DescribeSpotInstanceRequests', 

1980 'ec2:DescribeTags', 

1981 'ec2:CreateTags') 

1982 

1983 MAX_TAG_COUNT = 50 

1984 

1985 def process(self, instances): 

1986 instances = [ 

1987 i for i in instances if i['InstanceLifecycle'] == 'spot'] 

1988 if not len(instances): 

1989 self.log.warning( 

1990 "action:%s no spot instances found, implicit filter by action" % ( 

1991 self.__class__.__name__.lower())) 

1992 return 

1993 

1994 client = utils.local_session( 

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

1996 

1997 request_instance_map = {} 

1998 for i in instances: 

1999 request_instance_map.setdefault( 

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

2001 

2002 # ... and describe the corresponding spot requests ... 

2003 requests = client.describe_spot_instance_requests( 

2004 Filters=[{ 

2005 'Name': 'spot-instance-request-id', 

2006 'Values': list(request_instance_map.keys())}]).get( 

2007 'SpotInstanceRequests', []) 

2008 

2009 updated = [] 

2010 for r in requests: 

2011 if not r.get('Tags'): 

2012 continue 

2013 updated.extend( 

2014 self.process_request_instances( 

2015 client, r, request_instance_map[r['SpotInstanceRequestId']])) 

2016 return updated 

2017 

2018 def process_request_instances(self, client, request, instances): 

2019 # Now we find the tags we can copy : either all, either those 

2020 # indicated with 'only_tags' parameter. 

2021 copy_keys = self.data.get('only_tags', []) 

2022 request_tags = {t['Key']: t['Value'] for t in request['Tags'] 

2023 if not t['Key'].startswith('aws:')} 

2024 if copy_keys: 

2025 for k in set(copy_keys).difference(request_tags): 

2026 del request_tags[k] 

2027 

2028 update_instances = [] 

2029 for i in instances: 

2030 instance_tags = {t['Key']: t['Value'] for t in i.get('Tags', [])} 

2031 # We may overwrite tags, but if the operation changes no tag, 

2032 # we will not proceed. 

2033 for k, v in request_tags.items(): 

2034 if k not in instance_tags or instance_tags[k] != v: 

2035 update_instances.append(i['InstanceId']) 

2036 

2037 if len(set(instance_tags) | set(request_tags)) > self.MAX_TAG_COUNT: 

2038 self.log.warning( 

2039 "action:%s instance:%s too many tags to copy (> 50)" % ( 

2040 self.__class__.__name__.lower(), 

2041 i['InstanceId'])) 

2042 continue 

2043 

2044 for iset in utils.chunks(update_instances, 20): 

2045 client.create_tags( 

2046 DryRun=self.manager.config.dryrun, 

2047 Resources=iset, 

2048 Tags=[{'Key': k, 'Value': v} for k, v in request_tags.items()]) 

2049 

2050 self.log.debug( 

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

2052 self.__class__.__name__.lower(), 

2053 update_instances)) 

2054 

2055 return update_instances 

2056 

2057 

2058# Valid EC2 Query Filters 

2059# http://docs.aws.amazon.com/AWSEC2/latest/CommandLineReference/ApiReference-cmd-DescribeInstances.html 

2060EC2_VALID_FILTERS = { 

2061 'architecture': ('i386', 'x86_64'), 

2062 'availability-zone': str, 

2063 'iam-instance-profile.arn': str, 

2064 'image-id': str, 

2065 'instance-id': str, 

2066 'instance-lifecycle': ('spot',), 

2067 'instance-state-name': ( 

2068 'pending', 

2069 'terminated', 

2070 'running', 

2071 'shutting-down', 

2072 'stopping', 

2073 'stopped'), 

2074 'instance.group-id': str, 

2075 'instance.group-name': str, 

2076 'tag-key': str, 

2077 'tag-value': str, 

2078 'tag:': str, 

2079 'tenancy': ('dedicated', 'default', 'host'), 

2080 'vpc-id': str} 

2081 

2082 

2083class QueryFilter: 

2084 

2085 @classmethod 

2086 def parse(cls, data): 

2087 results = [] 

2088 for d in data: 

2089 if not isinstance(d, dict): 

2090 raise ValueError( 

2091 "EC2 Query Filter Invalid structure %s" % d) 

2092 results.append(cls(d).validate()) 

2093 return results 

2094 

2095 def __init__(self, data): 

2096 self.data = data 

2097 self.key = None 

2098 self.value = None 

2099 

2100 def validate(self): 

2101 if not len(list(self.data.keys())) == 1: 

2102 raise PolicyValidationError( 

2103 "EC2 Query Filter Invalid %s" % self.data) 

2104 self.key = list(self.data.keys())[0] 

2105 self.value = list(self.data.values())[0] 

2106 

2107 if self.key not in EC2_VALID_FILTERS and not self.key.startswith( 

2108 'tag:'): 

2109 raise PolicyValidationError( 

2110 "EC2 Query Filter invalid filter name %s" % (self.data)) 

2111 

2112 if self.value is None: 

2113 raise PolicyValidationError( 

2114 "EC2 Query Filters must have a value, use tag-key" 

2115 " w/ tag name as value for tag present checks" 

2116 " %s" % self.data) 

2117 return self 

2118 

2119 def query(self): 

2120 value = self.value 

2121 if isinstance(self.value, str): 

2122 value = [self.value] 

2123 

2124 return {'Name': self.key, 'Values': value} 

2125 

2126 

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

2128class InstanceAttribute(ValueFilter): 

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

2130 

2131 Filters EC2 Instances with the given instance attribute 

2132 

2133 :Example: 

2134 

2135 .. code-block:: yaml 

2136 

2137 policies: 

2138 - name: ec2-unoptimized-ebs 

2139 resource: ec2 

2140 filters: 

2141 - type: instance-attribute 

2142 attribute: ebsOptimized 

2143 key: "Value" 

2144 value: false 

2145 """ 

2146 

2147 valid_attrs = ( 

2148 'instanceType', 

2149 'kernel', 

2150 'ramdisk', 

2151 'userData', 

2152 'disableApiTermination', 

2153 'instanceInitiatedShutdownBehavior', 

2154 'rootDeviceName', 

2155 'blockDeviceMapping', 

2156 'productCodes', 

2157 'sourceDestCheck', 

2158 'groupSet', 

2159 'ebsOptimized', 

2160 'sriovNetSupport', 

2161 'enaSupport') 

2162 

2163 schema = type_schema( 

2164 'instance-attribute', 

2165 rinherit=ValueFilter.schema, 

2166 attribute={'enum': valid_attrs}, 

2167 required=('attribute',)) 

2168 schema_alias = False 

2169 

2170 def get_permissions(self): 

2171 return ('ec2:DescribeInstanceAttribute',) 

2172 

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

2174 attribute = self.data['attribute'] 

2175 self.get_instance_attribute(resources, attribute) 

2176 return [resource for resource in resources 

2177 if self.match(resource['c7n:attribute-%s' % attribute])] 

2178 

2179 def get_instance_attribute(self, resources, attribute): 

2180 client = utils.local_session( 

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

2182 

2183 for resource in resources: 

2184 instance_id = resource['InstanceId'] 

2185 fetched_attribute = self.manager.retry( 

2186 client.describe_instance_attribute, 

2187 Attribute=attribute, 

2188 InstanceId=instance_id) 

2189 keys = list(fetched_attribute.keys()) 

2190 keys.remove('ResponseMetadata') 

2191 keys.remove('InstanceId') 

2192 resource['c7n:attribute-%s' % attribute] = fetched_attribute[ 

2193 keys[0]] 

2194 

2195 

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

2197class LaunchTemplate(query.QueryResourceManager): 

2198 

2199 class resource_type(query.TypeInfo): 

2200 id = 'LaunchTemplateId' 

2201 id_prefix = 'lt-' 

2202 name = 'LaunchTemplateName' 

2203 service = 'ec2' 

2204 date = 'CreateTime' 

2205 enum_spec = ( 

2206 'describe_launch_templates', 'LaunchTemplates', None) 

2207 filter_name = 'LaunchTemplateIds' 

2208 filter_type = 'list' 

2209 arn_type = "launch-template" 

2210 cfn_type = "AWS::EC2::LaunchTemplate" 

2211 

2212 def augment(self, resources): 

2213 client = utils.local_session( 

2214 self.session_factory).client('ec2') 

2215 template_versions = [] 

2216 for r in resources: 

2217 template_versions.extend( 

2218 client.describe_launch_template_versions( 

2219 LaunchTemplateId=r['LaunchTemplateId']).get( 

2220 'LaunchTemplateVersions', ())) 

2221 return template_versions 

2222 

2223 def get_arns(self, resources): 

2224 arns = [] 

2225 for r in resources: 

2226 arns.append(self.generate_arn(f"{r['LaunchTemplateId']}/{r['VersionNumber']}")) 

2227 return arns 

2228 

2229 def get_resources(self, rids, cache=True): 

2230 # Launch template versions have a compound primary key 

2231 # 

2232 # Support one of four forms of resource ids: 

2233 # 

2234 # - array of launch template ids 

2235 # - array of tuples (launch template id, version id) 

2236 # - array of dicts (with LaunchTemplateId and VersionNumber) 

2237 # - array of dicts (with LaunchTemplateId and LatestVersionNumber) 

2238 # 

2239 # If an alias version is given $Latest, $Default, the alias will be 

2240 # preserved as an annotation on the returned object 'c7n:VersionAlias' 

2241 if not rids: 

2242 return [] 

2243 

2244 t_versions = {} 

2245 if isinstance(rids[0], tuple): 

2246 for tid, tversion in rids: 

2247 t_versions.setdefault(tid, []).append(tversion) 

2248 elif isinstance(rids[0], dict): 

2249 for tinfo in rids: 

2250 t_versions.setdefault( 

2251 tinfo['LaunchTemplateId'], []).append( 

2252 tinfo.get('VersionNumber', tinfo.get('LatestVersionNumber'))) 

2253 elif isinstance(rids[0], str): 

2254 for tid in rids: 

2255 t_versions[tid] = [] 

2256 

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

2258 

2259 results = [] 

2260 # We may end up fetching duplicates on $Latest and $Version 

2261 for tid, tversions in t_versions.items(): 

2262 try: 

2263 ltv = client.describe_launch_template_versions( 

2264 LaunchTemplateId=tid, Versions=tversions).get( 

2265 'LaunchTemplateVersions') 

2266 except ClientError as e: 

2267 if e.response['Error']['Code'] == "InvalidLaunchTemplateId.NotFound": 

2268 continue 

2269 if e.response['Error']['Code'] == "InvalidLaunchTemplateId.VersionNotFound": 

2270 continue 

2271 raise 

2272 if not tversions: 

2273 tversions = [str(t['VersionNumber']) for t in ltv] 

2274 for tversion, t in zip(tversions, ltv): 

2275 if not tversion.isdigit(): 

2276 t['c7n:VersionAlias'] = tversion 

2277 results.append(t) 

2278 return results 

2279 

2280 def get_asg_templates(self, asgs): 

2281 templates = {} 

2282 for a in asgs: 

2283 t = None 

2284 if 'LaunchTemplate' in a: 

2285 t = a['LaunchTemplate'] 

2286 elif 'MixedInstancesPolicy' in a: 

2287 t = a['MixedInstancesPolicy'][ 

2288 'LaunchTemplate']['LaunchTemplateSpecification'] 

2289 if t is None: 

2290 continue 

2291 templates.setdefault( 

2292 (t['LaunchTemplateId'], 

2293 t.get('Version', '$Default')), []).append(a['AutoScalingGroupName']) 

2294 return templates 

2295 

2296 

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

2298class ReservedInstance(query.QueryResourceManager): 

2299 

2300 class resource_type(query.TypeInfo): 

2301 service = 'ec2' 

2302 name = id = 'ReservedInstancesId' 

2303 id_prefix = "" 

2304 date = 'Start' 

2305 enum_spec = ( 

2306 'describe_reserved_instances', 'ReservedInstances', None) 

2307 filter_name = 'ReservedInstancesIds' 

2308 filter_type = 'list' 

2309 arn_type = "reserved-instances" 

2310 

2311 

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

2313class DedicatedHost(query.QueryResourceManager): 

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

2315 """ 

2316 

2317 class resource_type(query.TypeInfo): 

2318 service = 'ec2' 

2319 name = id = 'HostId' 

2320 id_prefix = 'h-' 

2321 enum_spec = ('describe_hosts', 'Hosts', None) 

2322 arn_type = "dedicated-host" 

2323 filter_name = 'HostIds' 

2324 filter_type = 'list' 

2325 date = 'AllocationTime' 

2326 cfn_type = config_type = 'AWS::EC2::Host' 

2327 permissions_enum = ('ec2:DescribeHosts',) 

2328 

2329 

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

2331class SpotFleetRequest(query.QueryResourceManager): 

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

2333 """ 

2334 

2335 class resource_type(query.TypeInfo): 

2336 service = 'ec2' 

2337 name = id = 'SpotFleetRequestId' 

2338 id_prefix = 'sfr-' 

2339 enum_spec = ('describe_spot_fleet_requests', 'SpotFleetRequestConfigs', None) 

2340 filter_name = 'SpotFleetRequestIds' 

2341 filter_type = 'list' 

2342 date = 'CreateTime' 

2343 arn_type = 'spot-fleet-request' 

2344 config_type = cfn_type = 'AWS::EC2::SpotFleet' 

2345 permissions_enum = ('ec2:DescribeSpotFleetRequests',) 

2346 

2347 

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

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

2350 

2351 

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

2353class AutoscalingSpotFleetRequest(AutoscalingBase): 

2354 permissions = ( 

2355 'ec2:CreateTags', 

2356 'ec2:ModifySpotFleetRequest', 

2357 ) 

2358 

2359 service_namespace = 'ec2' 

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

2361 

2362 def get_resource_id(self, resource): 

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

2364 

2365 def get_resource_tag(self, resource, key): 

2366 if 'Tags' in resource: 

2367 for tag in resource['Tags']: 

2368 if tag['Key'] == key: 

2369 return tag['Value'] 

2370 return None 

2371 

2372 def get_resource_desired(self, resource): 

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

2374 

2375 def set_resource_desired(self, resource, desired): 

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

2377 client.modify_spot_fleet_request( 

2378 SpotFleetRequestId=resource['SpotFleetRequestId'], 

2379 TargetCapacity=desired, 

2380 ) 

2381 

2382 

2383@EC2.filter_registry.register('has-specific-managed-policy') 

2384class HasSpecificManagedPolicy(SpecificIamProfileManagedPolicy): 

2385 """Filter an EC2 instance that has an IAM instance profile that contains an IAM role that has 

2386 a specific managed IAM policy. If an EC2 instance does not have a profile or the profile 

2387 does not contain an IAM role, then it will be treated as not having the policy. 

2388 

2389 :example: 

2390 

2391 .. code-block:: yaml 

2392 

2393 policies: 

2394 - name: ec2-instance-has-admin-policy 

2395 resource: aws.ec2 

2396 filters: 

2397 - type: has-specific-managed-policy 

2398 value: admin-policy 

2399 

2400 :example: 

2401 

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

2403 attached policy matching a given list: 

2404 

2405 .. code-block:: yaml 

2406 

2407 policies: 

2408 - name: ec2-instance-with-selected-policies 

2409 resource: aws.ec2 

2410 filters: 

2411 - type: has-specific-managed-policy 

2412 op: in 

2413 value: 

2414 - AmazonS3FullAccess 

2415 - AWSOrganizationsFullAccess 

2416 

2417 :example: 

2418 

2419 Check for EC2 instances with instance profile roles that have 

2420 attached policy names matching a pattern: 

2421 

2422 .. code-block:: yaml 

2423 

2424 policies: 

2425 - name: ec2-instance-with-full-access-policies 

2426 resource: aws.ec2 

2427 filters: 

2428 - type: has-specific-managed-policy 

2429 op: glob 

2430 value: "*FullAccess" 

2431 

2432 Check for EC2 instances with instance profile roles that have 

2433 attached policy ARNs matching a pattern: 

2434 

2435 .. code-block:: yaml 

2436 

2437 policies: 

2438 - name: ec2-instance-with-aws-full-access-policies 

2439 resource: aws.ec2 

2440 filters: 

2441 - type: has-specific-managed-policy 

2442 key: PolicyArn 

2443 op: regex 

2444 value: "arn:aws:iam::aws:policy/.*FullAccess" 

2445 """ 

2446 

2447 permissions = ( 

2448 'iam:GetInstanceProfile', 

2449 'iam:ListInstanceProfiles', 

2450 'iam:ListAttachedRolePolicies') 

2451 

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

2453 client = utils.local_session(self.manager.session_factory).client('iam') 

2454 iam_profiles = self.manager.get_resource_manager('iam-profile').resources() 

2455 iam_profiles_mapping = {profile['Arn']: profile for profile in iam_profiles} 

2456 

2457 results = [] 

2458 for r in resources: 

2459 if r['State']['Name'] == 'terminated': 

2460 continue 

2461 instance_profile_arn = r.get('IamInstanceProfile', {}).get('Arn') 

2462 if not instance_profile_arn: 

2463 continue 

2464 

2465 profile = iam_profiles_mapping.get(instance_profile_arn) 

2466 if not profile: 

2467 continue 

2468 

2469 self.get_managed_policies(client, [profile]) 

2470 

2471 matched_keys = [k for k in profile[self.annotation_key] if self.match(k)] 

2472 self.merge_annotation(profile, self.matched_annotation_key, matched_keys) 

2473 if matched_keys: 

2474 results.append(r) 

2475 

2476 return results 

2477 

2478 

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

2480class CapacityReservation(query.QueryResourceManager): 

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

2482 """ 

2483 

2484 class resource_type(query.TypeInfo): 

2485 name = id = 'CapacityReservationId' 

2486 service = 'ec2' 

2487 enum_spec = ('describe_capacity_reservations', 

2488 'CapacityReservations', None) 

2489 

2490 id_prefix = 'cr-' 

2491 arn = "CapacityReservationArn" 

2492 filter_name = 'CapacityReservationIds' 

2493 filter_type = 'list' 

2494 cfn_type = 'AWS::EC2::CapacityReservation' 

2495 permissions_enum = ('ec2:DescribeCapacityReservations',)