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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

311 statements  

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 

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

300class CancelLaunchPermissions(BaseAction): 

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

302 

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

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

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

306 

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

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

309 

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

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

312 

313 :example: 

314 

315 .. code-block:: yaml 

316 

317 policies: 

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

319 resource: ami 

320 query: 

321 - ExecutableUsers: [self] 

322 - Owners: [] 

323 filters: 

324 - type: image-age 

325 days: 90 

326 actions: 

327 - type: cancel-launch-permission 

328 

329 """ 

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

331 

332 permissions = ('ec2:CancelImageLaunchPermission',) 

333 

334 def process(self, images): 

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

336 for i in images: 

337 self.process_image(client, i) 

338 

339 def process_image(self, client, image): 

340 client.cancel_image_launch_permission( 

341 ImageId=image['ImageId'], 

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

343 

344 

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

346class SetPermissions(BaseAction): 

347 """Set or remove AMI launch permissions 

348 

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

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

351 

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

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

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

355 an organization ARN, or an organizational unit ARN 

356 

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

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

359 whitelisted accounts: 

360 

361 :example: 

362 

363 .. code-block:: yaml 

364 

365 policies: 

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

367 resource: ami 

368 filters: 

369 - type: cross-account 

370 whitelist: 

371 - '112233445566' 

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

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

374 actions: 

375 - type: set-permissions 

376 remove: matched 

377 # To remove all permissions 

378 # - type: set-permissions 

379 # To remove public permissions 

380 # - type: set-permissions 

381 # remove: 

382 # - all 

383 # To remove specific permissions 

384 # - type: set-permissions 

385 # remove: 

386 # - '223344556677' 

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

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

389 # To set specific permissions 

390 # - type: set-permissions 

391 # remove: matched 

392 # add: 

393 # - '223344556677' 

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

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

396 """ 

397 

398 schema = type_schema( 

399 'set-permissions', 

400 remove={'oneOf': [ 

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

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

403 ]}, 

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

405 ) 

406 

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

408 

409 def validate(self): 

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

411 found = False 

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

413 if isinstance(f, AmiCrossAccountFilter): 

414 found = True 

415 break 

416 if not found: 

417 raise PolicyValidationError( 

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

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

420 

421 def process(self, images): 

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

423 for i in images: 

424 self.process_image(client, i) 

425 

426 def process_image(self, client, image): 

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

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

429 # Default is to remove all permissions 

430 if not to_add and not to_remove: 

431 return client.reset_image_attribute( 

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

433 remove = [] 

434 add = [] 

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

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

437 org_regex = re.compile( 

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

439 ) 

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

441 ou_regex = re.compile( 

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

443 ) 

444 if to_remove: 

445 if 'all' in to_remove: 

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

447 to_remove.remove('all') 

448 if to_remove == 'matched': 

449 to_remove = image.get(AmiCrossAccountFilter.annotation_key) 

450 if to_remove: 

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

452 if principals: 

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

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

455 if principals: 

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

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

458 if principals: 

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

460 

461 if to_add: 

462 if 'all' in to_add: 

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

464 to_add.remove('all') 

465 if to_add: 

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

467 if principals: 

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

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

470 if principals: 

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

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

473 if principals: 

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

475 

476 if remove: 

477 self.manager.retry(client.modify_image_attribute, 

478 ImageId=image['ImageId'], 

479 LaunchPermission={'Remove': remove}, 

480 OperationType='remove') 

481 

482 if add: 

483 self.manager.retry(client.modify_image_attribute, 

484 ImageId=image['ImageId'], 

485 LaunchPermission={'Add': add}, 

486 OperationType='add') 

487 

488 

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

490class Copy(BaseAction): 

491 """Action to copy AMIs with optional encryption 

492 

493 This action can copy AMIs while optionally encrypting or decrypting 

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

495 

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

497 

498 :example: 

499 

500 .. code-block:: yaml 

501 

502 policies: 

503 - name: ami-ensure-encrypted 

504 resource: ami 

505 filters: 

506 - type: value 

507 key: encrypted 

508 value: true 

509 actions: 

510 - type: copy 

511 encrypt: true 

512 key-id: 00000000-0000-0000-0000-000000000000 

513 """ 

514 

515 permissions = ('ec2:CopyImage',) 

516 schema = { 

517 'type': 'object', 

518 'additionalProperties': False, 

519 'properties': { 

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

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

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

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

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

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

526 } 

527 } 

528 

529 def process(self, images): 

530 session = local_session(self.manager.session_factory) 

531 client = session.client( 

532 'ec2', 

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

534 

535 for image in images: 

536 client.copy_image( 

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

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

539 SourceRegion=session.region_name, 

540 SourceImageId=image['ImageId'], 

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

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

543 

544 

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

546class ImageAgeFilter(AgeFilter): 

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

548 

549 :example: 

550 

551 .. code-block:: yaml 

552 

553 policies: 

554 - name: ami-remove-launch-permissions 

555 resource: ami 

556 filters: 

557 - type: image-age 

558 days: 30 

559 """ 

560 

561 date_attribute = "CreationDate" 

562 schema = type_schema( 

563 'image-age', 

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

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

566 

567 

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

569class ImageUnusedFilter(Filter): 

570 """Filters images based on usage 

571 

572 true: image has no instances spawned from it 

573 false: image has instances spawned from it 

574 

575 :example: 

576 

577 .. code-block:: yaml 

578 

579 policies: 

580 - name: ami-unused 

581 resource: ami 

582 filters: 

583 - type: unused 

584 value: true 

585 """ 

586 

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

588 

589 def get_permissions(self): 

590 return list(itertools.chain(*[ 

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

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

593 

594 def _pull_asg_images(self): 

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

596 image_ids = set() 

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

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

599 

600 if lcfgs: 

601 image_ids.update([ 

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

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

604 

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

606 for tversion in tmpl_mgr.get_resources( 

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

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

609 return image_ids 

610 

611 def _pull_ec2_images(self): 

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

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

614 

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

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

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

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

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

620 

621 

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

623class AmiCrossAccountFilter(CrossAccountAccessFilter): 

624 

625 schema = type_schema( 

626 'cross-account', 

627 # white list accounts 

628 whitelist_from=ValuesFrom.schema, 

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

630 

631 permissions = ('ec2:DescribeImageAttribute',) 

632 annotation_key = 'c7n:CrossAccountViolations' 

633 

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

635 results = [] 

636 for r in resource_set: 

637 attrs = self.manager.retry( 

638 client.describe_image_attribute, 

639 ImageId=r['ImageId'], 

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

641 r['c7n:LaunchPermissions'] = attrs 

642 image_accounts = { 

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

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

645 for a in attrs 

646 } 

647 delta_accounts = image_accounts.difference(accounts) 

648 if delta_accounts: 

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

650 results.append(r) 

651 return results 

652 

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

654 results = [] 

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

656 accounts = self.get_accounts() 

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

658 futures = [] 

659 for resource_set in chunks(resources, 20): 

660 futures.append( 

661 w.submit( 

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

663 for f in as_completed(futures): 

664 if f.exception(): 

665 self.log.error( 

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

667 f.exception())) 

668 continue 

669 results.extend(f.result()) 

670 return results 

671 

672 

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

674class ImageAttribute(ValueFilter): 

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

676 

677 Filters AMI's with the given AMI attribute 

678 

679 :example: 

680 

681 .. code-block:: yaml 

682 

683 policies: 

684 - name: ami-unused-recently 

685 resource: ami 

686 filters: 

687 - type: image-attribute 

688 attribute: lastLaunchedTime 

689 key: "Value" 

690 op: gte 

691 value_type: age 

692 value: 30 

693 """ 

694 

695 valid_attrs = ( 

696 'description', 

697 'kernel', 

698 'ramdisk', 

699 'launchPermissions', 

700 'productCodes', 

701 'blockDeviceMapping', 

702 'sriovNetSupport', 

703 'bootMode', 

704 'tpmSupport', 

705 'uefiData', 

706 'lastLaunchedTime', 

707 'imdsSupport' 

708 ) 

709 

710 schema = type_schema( 

711 'image-attribute', 

712 rinherit=ValueFilter.schema, 

713 attribute={'enum': valid_attrs}, 

714 required=('attribute',)) 

715 schema_alias = False 

716 

717 def get_permissions(self): 

718 return ('ec2:DescribeImageAttribute',) 

719 

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

721 attribute = self.data['attribute'] 

722 self.get_image_attribute(resources, attribute) 

723 return [resource for resource in resources 

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

725 

726 def get_image_attribute(self, resources, attribute): 

727 client = local_session( 

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

729 

730 for resource in resources: 

731 image_id = resource['ImageId'] 

732 fetched_attribute = self.manager.retry( 

733 client.describe_image_attribute, 

734 ImageId=image_id, 

735 Attribute=attribute) 

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

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