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

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

4import datetime 

5from datetime import timedelta 

6from dateutil.tz import tzutc 

7 

8import itertools 

9import logging 

10 

11from concurrent.futures import as_completed 

12 

13from c7n.actions import BaseAction 

14from c7n.exceptions import ClientError, PolicyValidationError 

15from c7n.filters import ( 

16 AgeFilter, ValueFilter, Filter, CrossAccountAccessFilter) 

17from c7n.manager import resources 

18from c7n.query import QueryResourceManager, DescribeSource, TypeInfo 

19from c7n.resolver import ValuesFrom 

20from c7n.utils import ( 

21 local_session, 

22 type_schema, 

23 chunks, 

24 merge_dict_list, 

25 parse_date, 

26 jmespath_compile 

27) 

28from c7n import deprecated 

29 

30 

31log = logging.getLogger('custodian.ami') 

32 

33 

34class DescribeImageSource(DescribeSource): 

35 

36 def get_resources(self, ids, cache=True): 

37 while ids: 

38 try: 

39 return super(DescribeImageSource, self).get_resources(ids, cache) 

40 except ClientError as e: 

41 bad_ami_ids = ErrorHandler.extract_bad_ami(e) 

42 if bad_ami_ids: 

43 for b in bad_ami_ids: 

44 ids.remove(b) 

45 continue 

46 raise 

47 return [] 

48 

49 

50@resources.register('ami') 

51class AMI(QueryResourceManager): 

52 

53 class resource_type(TypeInfo): 

54 service = 'ec2' 

55 arn_type = 'image' 

56 enum_spec = ( 

57 'describe_images', 'Images', None) 

58 id = 'ImageId' 

59 filter_name = 'ImageIds' 

60 filter_type = 'list' 

61 name = 'Name' 

62 date = 'CreationDate' 

63 id_prefix = "ami-" 

64 

65 source_mapping = { 

66 'describe': DescribeImageSource 

67 } 

68 

69 def resources(self, query=None): 

70 if query is None and 'query' in self.data: 

71 query = merge_dict_list(self.data['query']) 

72 elif query is None: 

73 query = {} 

74 if query.get('Owners') is None: 

75 query['Owners'] = ['self'] 

76 return super(AMI, self).resources(query=query) 

77 

78 

79class ErrorHandler: 

80 

81 @staticmethod 

82 def extract_bad_ami(e): 

83 """Handle various client side errors when describing images""" 

84 msg = e.response['Error']['Message'] 

85 error = e.response['Error']['Code'] 

86 e_ami_ids = None 

87 if error == 'InvalidAMIID.NotFound': 

88 e_ami_ids = [ 

89 e_ami_id.strip() for e_ami_id 

90 in msg[msg.find("'[") + 2:msg.rfind("]'")].split(',')] 

91 log.warning("Image not found %s" % e_ami_ids) 

92 elif error == 'InvalidAMIID.Malformed': 

93 e_ami_ids = [msg[msg.find('"') + 1:msg.rfind('"')]] 

94 log.warning("Image id malformed %s" % e_ami_ids) 

95 return e_ami_ids 

96 

97 

98@AMI.action_registry.register('deregister') 

99class Deregister(BaseAction): 

100 """Action to deregister AMI 

101 

102 To prevent deregistering all AMI, it is advised to use in conjunction with 

103 a filter (such as image-age) 

104 

105 :example: 

106 

107 .. code-block:: yaml 

108 

109 policies: 

110 - name: ami-deregister-old 

111 resource: ami 

112 filters: 

113 - type: image-age 

114 days: 90 

115 actions: 

116 - deregister 

117 """ 

118 

119 schema = type_schema('deregister', **{'delete-snapshots': {'type': 'boolean'}}) 

120 permissions = ('ec2:DeregisterImage',) 

121 snap_expr = jmespath_compile('BlockDeviceMappings[].Ebs.SnapshotId') 

122 

123 def process(self, images): 

124 client = local_session(self.manager.session_factory).client('ec2') 

125 image_count = len(images) 

126 images = self.filter_resources(images, 'OwnerId', self.manager.ctx.options.account_id) 

127 if len(images) != image_count: 

128 self.log.info("Implicitly filtered %d non owned images", image_count - len(images)) 

129 

130 for i in images: 

131 self.manager.retry(client.deregister_image, ImageId=i['ImageId']) 

132 

133 if not self.data.get('delete-snapshots'): 

134 continue 

135 snap_ids = self.snap_expr.search(i) or () 

136 for s in snap_ids: 

137 try: 

138 self.manager.retry(client.delete_snapshot, SnapshotId=s) 

139 except ClientError as e: 

140 if e.response['Error']['Code'] == 'InvalidSnapshot.InUse': 

141 continue 

142 

143 

144@AMI.action_registry.register('set-deprecation') 

145class SetDeprecation(BaseAction): 

146 """Action to enable or disable AMI deprecation 

147 

148 To prevent deprecation of all AMIs, it is advised to use in conjunction with 

149 a filter (such as image-age) 

150 

151 :example: 

152 

153 .. code-block:: yaml 

154 

155 policies: 

156 - name: ami-deprecate-old 

157 resource: ami 

158 filters: 

159 - type: image-age 

160 days: 30 

161 actions: 

162 - type: set-deprecation 

163 #Number of days from AMI creation 

164 age: 90 

165 #Number of days from now 

166 #days: 90 

167 #Specific date/time 

168 #date: "2023-11-30" 

169 

170 """ 

171 

172 schema = type_schema( 

173 'set-deprecation', 

174 date={'type': 'string'}, 

175 days={'type': 'integer'}, 

176 age={'type': 'integer'}) 

177 permissions = ('ec2:EnableImageDeprecation', 'ec2:DisableImageDeprecation') 

178 dep_date = None 

179 dep_age = None 

180 

181 def validate(self): 

182 try: 

183 if 'date' in self.data: 

184 self.dep_date = parse_date(self.data.get('date')) 

185 if not self.dep_date: 

186 raise PolicyValidationError( 

187 "policy:%s filter:%s has invalid date format" % ( 

188 self.manager.ctx.policy.name, self.type)) 

189 elif 'days' in self.data: 

190 self.dep_date = (datetime.datetime.now(tz=tzutc()) + 

191 timedelta(days=int(self.data.get('days')))) 

192 elif 'age' in self.data: 

193 self.dep_age = (int(self.data.get('age'))) 

194 except (ValueError, OverflowError): 

195 raise PolicyValidationError( 

196 "policy:%s filter:%s has invalid time interval" % ( 

197 self.manager.ctx.policy.name, self.type)) 

198 

199 def process(self, images): 

200 client = local_session(self.manager.session_factory).client('ec2') 

201 image_count = len(images) 

202 images = self.filter_resources(images, 'OwnerId', self.manager.ctx.options.account_id) 

203 if len(images) != image_count: 

204 self.log.info("Implicitly filtered %d non owned images", image_count - len(images)) 

205 for i in images: 

206 if not self.dep_date and not self.dep_age: 

207 self.manager.retry(client.disable_image_deprecation, ImageId=i['ImageId']) 

208 else: 

209 if self.dep_age: 

210 date = parse_date(i['CreationDate']) + timedelta(days=self.dep_age) 

211 else: 

212 date = self.dep_date 

213 # Hack because AWS won't let you set a deprecation time in the 

214 # past - set to now + 1 minute if the time is in the past 

215 if date < datetime.datetime.now(tz=tzutc()): 

216 odate = str(date) 

217 date = datetime.datetime.now(tz=tzutc()) + timedelta(minutes=1) 

218 log.warning("Deprecation time %s is in the past for Image %s. Setting to %s.", 

219 odate, i['ImageId'], date) 

220 self.manager.retry(client.enable_image_deprecation, 

221 ImageId=i['ImageId'], DeprecateAt=date) 

222 

223 

224@AMI.action_registry.register('remove-launch-permissions') 

225class RemoveLaunchPermissions(BaseAction): 

226 """Action to remove the ability to launch an instance from an AMI 

227 

228 DEPRECATED - use set-permissions instead to support AWS Organizations 

229 sharing as well as adding permissions 

230 

231 This action will remove any launch permissions granted to other 

232 AWS accounts from the image, leaving only the owner capable of 

233 launching it 

234 

235 :example: 

236 

237 .. code-block:: yaml 

238 

239 policies: 

240 - name: ami-stop-share-old 

241 resource: ami 

242 filters: 

243 - type: image-age 

244 days: 60 

245 actions: 

246 - type: remove-launch-permissions 

247 

248 """ 

249 deprecations = ( 

250 deprecated.action("use set-permissions instead with 'remove' attribute"), 

251 ) 

252 schema = type_schema( 

253 'remove-launch-permissions', 

254 accounts={'oneOf': [ 

255 {'enum': ['matched']}, 

256 {'type': 'string', 'minLength': 12, 'maxLength': 12}]}) 

257 

258 permissions = ('ec2:ResetImageAttribute', 'ec2:ModifyImageAttribute',) 

259 

260 def validate(self): 

261 if 'accounts' in self.data and self.data['accounts'] == 'matched': 

262 found = False 

263 for f in self.manager.iter_filters(): 

264 if isinstance(f, AmiCrossAccountFilter): 

265 found = True 

266 break 

267 if not found: 

268 raise PolicyValidationError( 

269 "policy:%s filter:%s with matched requires cross-account filter" % ( 

270 self.manager.ctx.policy.name, self.type)) 

271 

272 def process(self, images): 

273 client = local_session(self.manager.session_factory).client('ec2') 

274 for i in images: 

275 self.process_image(client, i) 

276 

277 def process_image(self, client, image): 

278 accounts = self.data.get('accounts') 

279 if not accounts: 

280 return client.reset_image_attribute( 

281 ImageId=image['ImageId'], Attribute="launchPermission") 

282 if accounts == 'matched': 

283 accounts = image.get(AmiCrossAccountFilter.annotation_key) 

284 if not accounts: 

285 return 

286 remove = [] 

287 if 'all' in accounts: 

288 remove.append({'Group': 'all'}) 

289 accounts.remove('all') 

290 remove.extend([{'UserId': a} for a in accounts if not a.startswith('arn:')]) 

291 if not remove: 

292 return 

293 client.modify_image_attribute( 

294 ImageId=image['ImageId'], 

295 LaunchPermission={'Remove': remove}, 

296 OperationType='remove') 

297 

298@AMI.action_registry.register('cancel-launch-permission') 

299class CancelLaunchPermissions(BaseAction): 

300 """Action to cancel this account's access to another another account's shared AMI 

301 

302 If another AWS account shares an image with your account, and you 

303 no longer want to allow its use in your account, this action will 

304 remove the permission for your account to laucnh from the image. 

305 

306 As this is not reversible without accessing the AMI source account, it defaults 

307 to running in dryrun mode. Set dryrun to false to enforce. 

308 

309 Note this does not apply to AMIs shared by Organization or OU. 

310 https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/cancel-sharing-an-AMI.html 

311 

312 :example: 

313 

314 .. code-block:: yaml 

315 

316 policies: 

317 - name: ami-cancel-share-to-me-old 

318 resource: ami 

319 query: 

320 - ExecutableUsers: [self] 

321 - Owners: [] 

322 filters: 

323 - type: image-age 

324 days: 90 

325 actions: 

326 - type: cancel-launch-permission 

327 

328 """ 

329 schema = type_schema('cancel-launch-permission', dryrun={'type': 'boolean'}) 

330 

331 permissions = ('ec2:CancelImageLaunchPermission',) 

332 

333 def process(self, images): 

334 client = local_session(self.manager.session_factory).client('ec2') 

335 for i in images: 

336 self.process_image(client, i) 

337 

338 def process_image(self, client, image): 

339 client.cancel_image_launch_permission( 

340 ImageId=image['ImageId'], 

341 DryRun=self.data.get('dryrun', True)) 

342 

343 

344@AMI.action_registry.register('set-permissions') 

345class SetPermissions(BaseAction): 

346 """Set or remove AMI launch permissions 

347 

348 This action will add or remove launch permissions granted to other 

349 AWS accounts, organizations or organizational units from the image. 

350 

351 Use the 'add' and 'remove' parameters to control which principals 

352 to add or remove, respectively. The default is to remove any permissions 

353 granted to other AWS accounts. Principals can be an AWS account id, 

354 an organization ARN, or an organizational unit ARN 

355 

356 Use 'remove: matched' in combination with the 'cross-account' filter 

357 for more flexible removal options such as preserving access for a set of 

358 whitelisted accounts: 

359 

360 :example: 

361 

362 .. code-block:: yaml 

363 

364 policies: 

365 - name: ami-share-remove-cross-account 

366 resource: ami 

367 filters: 

368 - type: cross-account 

369 whitelist: 

370 - '112233445566' 

371 - 'arn:aws:organizations::112233445566:organization/o-xxyyzzaabb' 

372 - 'arn:aws:organizations::112233445566:ou/o-xxyyzzaabb/ou-xxyy-aabbccdd' 

373 actions: 

374 - type: set-permissions 

375 remove: matched 

376 # To remove all permissions 

377 # - type: set-permissions 

378 # To remove public permissions 

379 # - type: set-permissions 

380 # remove: 

381 # - all 

382 # To remove specific permissions 

383 # - type: set-permissions 

384 # remove: 

385 # - '223344556677' 

386 # - 'arn:aws:organizations::112233445566:organization/o-zzyyxxbbaa' 

387 # - 'arn:aws:organizations::112233445566:ou/o-zzyyxxbbaa/ou-xxyy-ddccbbaa' 

388 # To set specific permissions 

389 # - type: set-permissions 

390 # remove: matched 

391 # add: 

392 # - '223344556677' 

393 # - 'arn:aws:organizations::112233445566:organization/o-zzyyxxbbaa' 

394 # - 'arn:aws:organizations::112233445566:ou/o-zzyyxxbbaa/ou-xxyy-ddccbbaa' 

395 """ 

396 

397 schema = type_schema( 

398 'set-permissions', 

399 remove={'oneOf': [ 

400 {'enum': ['matched']}, 

401 {'type': 'array', 'items': {'type': 'string'}} 

402 ]}, 

403 add={'type': 'array', 'items': {'type': 'string'}} 

404 ) 

405 

406 permissions = ('ec2:ResetImageAttribute', 'ec2:ModifyImageAttribute',) 

407 

408 def validate(self): 

409 if self.data.get('remove') == 'matched': 

410 found = False 

411 for f in self.manager.iter_filters(): 

412 if isinstance(f, AmiCrossAccountFilter): 

413 found = True 

414 break 

415 if not found: 

416 raise PolicyValidationError( 

417 "policy:%s filter:%s with matched requires cross-account filter" % ( 

418 self.manager.ctx.policy.name, self.type)) 

419 

420 def process(self, images): 

421 client = local_session(self.manager.session_factory).client('ec2') 

422 for i in images: 

423 self.process_image(client, i) 

424 

425 def process_image(self, client, image): 

426 to_add = self.data.get('add') 

427 to_remove = self.data.get('remove') 

428 # Default is to remove all permissions 

429 if not to_add and not to_remove: 

430 return client.reset_image_attribute( 

431 ImageId=image['ImageId'], Attribute="launchPermission") 

432 remove = [] 

433 add = [] 

434 account_regex = re.compile('\\d{12}') 

435 # https://docs.aws.amazon.com/organizations/latest/APIReference/API_Organization.html 

436 org_regex = re.compile( 

437 r'arn:[a-zA-Z-]+:organizations::\d{12}:organization\/o-[a-z0-9]{10,32}' 

438 ) 

439 # https://docs.aws.amazon.com/organizations/latest/APIReference/API_OrganizationalUnit.html 

440 ou_regex = re.compile( 

441 r'arn:[a-zA-Z-]+:organizations::\d{12}:ou\/o-[a-z0-9]{10,32}\/ou-[0-9a-z]{4,32}-[0-9a-z]{8,32}' 

442 ) 

443 if to_remove: 

444 if 'all' in to_remove: 

445 remove.append({'Group': 'all'}) 

446 to_remove.remove('all') 

447 if to_remove == 'matched': 

448 to_remove = image.get(AmiCrossAccountFilter.annotation_key) 

449 if to_remove: 

450 principals = [v for v in to_remove if account_regex.match(v)] 

451 if principals: 

452 remove.extend([{'UserId': a} for a in principals]) 

453 principals = [v for v in to_remove if org_regex.match(v)] 

454 if principals: 

455 remove.extend([{'OrganizationArn': a} for a in principals]) 

456 principals = [v for v in to_remove if ou_regex.match(v)] 

457 if principals: 

458 remove.extend([{'OrganizationalUnitArn': a} for a in principals]) 

459 

460 if to_add: 

461 if 'all' in to_add: 

462 add.append({'Group': 'all'}) 

463 to_add.remove('all') 

464 if to_add: 

465 principals = [v for v in to_add if account_regex.match(v)] 

466 if principals: 

467 add.extend([{'UserId': a} for a in principals]) 

468 principals = [v for v in to_add if org_regex.match(v)] 

469 if principals: 

470 add.extend([{'OrganizationArn': a} for a in principals]) 

471 principals = [v for v in to_add if ou_regex.match(v)] 

472 if principals: 

473 add.extend([{'OrganizationalUnitArn': a} for a in principals]) 

474 

475 if remove: 

476 self.manager.retry(client.modify_image_attribute, 

477 ImageId=image['ImageId'], 

478 LaunchPermission={'Remove': remove}, 

479 OperationType='remove') 

480 

481 if add: 

482 self.manager.retry(client.modify_image_attribute, 

483 ImageId=image['ImageId'], 

484 LaunchPermission={'Add': add}, 

485 OperationType='add') 

486 

487 

488@AMI.action_registry.register('copy') 

489class Copy(BaseAction): 

490 """Action to copy AMIs with optional encryption 

491 

492 This action can copy AMIs while optionally encrypting or decrypting 

493 the target AMI. It is advised to use in conjunction with a filter. 

494 

495 Note there is a max in flight of 5 per account/region. 

496 

497 :example: 

498 

499 .. code-block:: yaml 

500 

501 policies: 

502 - name: ami-ensure-encrypted 

503 resource: ami 

504 filters: 

505 - type: value 

506 key: encrypted 

507 value: true 

508 actions: 

509 - type: copy 

510 encrypt: true 

511 key-id: 00000000-0000-0000-0000-000000000000 

512 """ 

513 

514 permissions = ('ec2:CopyImage',) 

515 schema = { 

516 'type': 'object', 

517 'additionalProperties': False, 

518 'properties': { 

519 'type': {'enum': ['copy']}, 

520 'name': {'type': 'string'}, 

521 'description': {'type': 'string'}, 

522 'region': {'type': 'string'}, 

523 'encrypt': {'type': 'boolean'}, 

524 'key-id': {'type': 'string'} 

525 } 

526 } 

527 

528 def process(self, images): 

529 session = local_session(self.manager.session_factory) 

530 client = session.client( 

531 'ec2', 

532 region_name=self.data.get('region', None)) 

533 

534 for image in images: 

535 client.copy_image( 

536 Name=self.data.get('name', image['Name']), 

537 Description=self.data.get('description', image['Description']), 

538 SourceRegion=session.region_name, 

539 SourceImageId=image['ImageId'], 

540 Encrypted=self.data.get('encrypt', False), 

541 KmsKeyId=self.data.get('key-id', '')) 

542 

543 

544@AMI.filter_registry.register('image-age') 

545class ImageAgeFilter(AgeFilter): 

546 """Filters images based on the age (in days) 

547 

548 :example: 

549 

550 .. code-block:: yaml 

551 

552 policies: 

553 - name: ami-remove-launch-permissions 

554 resource: ami 

555 filters: 

556 - type: image-age 

557 days: 30 

558 """ 

559 

560 date_attribute = "CreationDate" 

561 schema = type_schema( 

562 'image-age', 

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

564 days={'type': 'number', 'minimum': 0}) 

565 

566 

567@AMI.filter_registry.register('unused') 

568class ImageUnusedFilter(Filter): 

569 """Filters images based on usage 

570 

571 true: image has no instances spawned from it 

572 false: image has instances spawned from it 

573 

574 :example: 

575 

576 .. code-block:: yaml 

577 

578 policies: 

579 - name: ami-unused 

580 resource: ami 

581 filters: 

582 - type: unused 

583 value: true 

584 """ 

585 

586 schema = type_schema('unused', value={'type': 'boolean'}) 

587 

588 def get_permissions(self): 

589 return list(itertools.chain(*[ 

590 self.manager.get_resource_manager(m).get_permissions() 

591 for m in ('asg', 'launch-config', 'ec2')])) 

592 

593 def _pull_asg_images(self): 

594 asgs = self.manager.get_resource_manager('asg').resources() 

595 image_ids = set() 

596 lcfgs = set(a['LaunchConfigurationName'] for a in asgs if 'LaunchConfigurationName' in a) 

597 lcfg_mgr = self.manager.get_resource_manager('launch-config') 

598 

599 if lcfgs: 

600 image_ids.update([ 

601 lcfg['ImageId'] for lcfg in lcfg_mgr.resources() 

602 if lcfg['LaunchConfigurationName'] in lcfgs]) 

603 

604 tmpl_mgr = self.manager.get_resource_manager('launch-template-version') 

605 for tversion in tmpl_mgr.get_resources( 

606 list(tmpl_mgr.get_asg_templates(asgs).keys())): 

607 image_ids.add(tversion['LaunchTemplateData'].get('ImageId')) 

608 return image_ids 

609 

610 def _pull_ec2_images(self): 

611 ec2_manager = self.manager.get_resource_manager('ec2') 

612 return {i['ImageId'] for i in ec2_manager.resources()} 

613 

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

615 images = self._pull_ec2_images().union(self._pull_asg_images()) 

616 if self.data.get('value', True): 

617 return [r for r in resources if r['ImageId'] not in images] 

618 return [r for r in resources if r['ImageId'] in images] 

619 

620 

621@AMI.filter_registry.register('cross-account') 

622class AmiCrossAccountFilter(CrossAccountAccessFilter): 

623 

624 schema = type_schema( 

625 'cross-account', 

626 # white list accounts 

627 whitelist_from=ValuesFrom.schema, 

628 whitelist={'type': 'array', 'items': {'type': 'string'}}) 

629 

630 permissions = ('ec2:DescribeImageAttribute',) 

631 annotation_key = 'c7n:CrossAccountViolations' 

632 

633 def process_resource_set(self, client, accounts, resource_set): 

634 results = [] 

635 for r in resource_set: 

636 attrs = self.manager.retry( 

637 client.describe_image_attribute, 

638 ImageId=r['ImageId'], 

639 Attribute='launchPermission')['LaunchPermissions'] 

640 r['c7n:LaunchPermissions'] = attrs 

641 image_accounts = { 

642 a.get('Group') or a.get('UserId') or 

643 a.get('OrganizationArn') or a.get('OrganizationalUnitArn') 

644 for a in attrs 

645 } 

646 delta_accounts = image_accounts.difference(accounts) 

647 if delta_accounts: 

648 r[self.annotation_key] = list(delta_accounts) 

649 results.append(r) 

650 return results 

651 

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

653 results = [] 

654 client = local_session(self.manager.session_factory).client('ec2') 

655 accounts = self.get_accounts() 

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

657 futures = [] 

658 for resource_set in chunks(resources, 20): 

659 futures.append( 

660 w.submit( 

661 self.process_resource_set, client, accounts, resource_set)) 

662 for f in as_completed(futures): 

663 if f.exception(): 

664 self.log.error( 

665 "Exception checking cross account access \n %s" % ( 

666 f.exception())) 

667 continue 

668 results.extend(f.result()) 

669 return results 

670 

671 

672@AMI.filter_registry.register('image-attribute') 

673class ImageAttribute(ValueFilter): 

674 """AMI Image Value Filter on a given image attribute. 

675 

676 Filters AMI's with the given AMI attribute 

677 

678 :example: 

679 

680 .. code-block:: yaml 

681 

682 policies: 

683 - name: ami-unused-recently 

684 resource: ami 

685 filters: 

686 - type: image-attribute 

687 attribute: lastLaunchedTime 

688 key: "Value" 

689 op: gte 

690 value_type: age 

691 value: 30 

692 """ 

693 

694 valid_attrs = ( 

695 'description', 

696 'kernel', 

697 'ramdisk', 

698 'launchPermissions', 

699 'productCodes', 

700 'blockDeviceMapping', 

701 'sriovNetSupport', 

702 'bootMode', 

703 'tpmSupport', 

704 'uefiData', 

705 'lastLaunchedTime', 

706 'imdsSupport' 

707 ) 

708 

709 schema = type_schema( 

710 'image-attribute', 

711 rinherit=ValueFilter.schema, 

712 attribute={'enum': valid_attrs}, 

713 required=('attribute',)) 

714 schema_alias = False 

715 

716 def get_permissions(self): 

717 return ('ec2:DescribeImageAttribute',) 

718 

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

720 attribute = self.data['attribute'] 

721 self.get_image_attribute(resources, attribute) 

722 return [resource for resource in resources 

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

724 

725 def get_image_attribute(self, resources, attribute): 

726 client = local_session( 

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

728 

729 for resource in resources: 

730 image_id = resource['ImageId'] 

731 fetched_attribute = self.manager.retry( 

732 client.describe_image_attribute, 

733 ImageId=image_id, 

734 Attribute=attribute) 

735 keys = set(fetched_attribute) - {'ResponseMetadata', 'ImageId'} 

736 resource['c7n:attribute-%s' % attribute] = fetched_attribute[keys.pop()]