Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/c7n/resources/asg.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

851 statements  

1# Copyright The Cloud Custodian Authors. 

2# SPDX-License-Identifier: Apache-2.0 

3from botocore.client import ClientError 

4 

5from collections import Counter 

6from concurrent.futures import as_completed 

7 

8from dateutil.parser import parse 

9 

10import itertools 

11import time 

12 

13from c7n.actions import Action, AutoTagUser 

14from c7n.exceptions import PolicyValidationError 

15from c7n.filters import ValueFilter, AgeFilter, Filter 

16from c7n.filters.offhours import OffHour, OnHour 

17import c7n.filters.vpc as net_filters 

18import c7n.policy 

19 

20from c7n.manager import resources 

21from c7n import query 

22from c7n.resources.securityhub import PostFinding 

23from c7n.tags import TagActionFilter, DEFAULT_TAG, TagCountFilter, TagTrim, TagDelayedAction 

24from c7n.utils import ( 

25 FormatDate, local_session, type_schema, chunks, get_retry, select_keys) 

26 

27from .ec2 import deserialize_user_data 

28 

29 

30@resources.register('asg') 

31class ASG(query.QueryResourceManager): 

32 

33 class resource_type(query.TypeInfo): 

34 service = 'autoscaling' 

35 arn = 'AutoScalingGroupARN' 

36 arn_type = 'autoScalingGroup' 

37 arn_separator = ":" 

38 id = name = 'AutoScalingGroupName' 

39 config_id = 'AutoScalingGroupARN' 

40 date = 'CreatedTime' 

41 dimension = 'AutoScalingGroupName' 

42 enum_spec = ('describe_auto_scaling_groups', 'AutoScalingGroups', None) 

43 filter_name = 'AutoScalingGroupNames' 

44 filter_type = 'list' 

45 config_type = 'AWS::AutoScaling::AutoScalingGroup' 

46 cfn_type = 'AWS::AutoScaling::AutoScalingGroup' 

47 permissions_augment = ("autoscaling:DescribeTags",) 

48 

49 default_report_fields = ( 

50 'AutoScalingGroupName', 

51 'CreatedTime', 

52 'LaunchConfigurationName', 

53 'count:Instances', 

54 'DesiredCapacity', 

55 'HealthCheckType', 

56 'list:LoadBalancerNames', 

57 ) 

58 

59 retry = staticmethod(get_retry(('ResourceInUse', 'Throttling',))) 

60 

61 

62ASG.filter_registry.register('offhour', OffHour) 

63ASG.filter_registry.register('onhour', OnHour) 

64ASG.filter_registry.register('tag-count', TagCountFilter) 

65ASG.filter_registry.register('marked-for-op', TagActionFilter) 

66ASG.filter_registry.register('network-location', net_filters.NetworkLocation) 

67 

68 

69class LaunchInfo: 

70 

71 permissions = ("ec2:DescribeLaunchTemplateVersions", 

72 "autoscaling:DescribeLaunchConfigurations",) 

73 

74 def __init__(self, manager): 

75 self.manager = manager 

76 

77 def initialize(self, asgs): 

78 self.templates = self.get_launch_templates(asgs) 

79 self.configs = self.get_launch_configs(asgs) 

80 return self 

81 

82 def get_launch_templates(self, asgs): 

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

84 # template ids include version identifiers 

85 template_ids = list(tmpl_mgr.get_asg_templates(asgs)) 

86 if not template_ids: 

87 return {} 

88 return { 

89 (t['LaunchTemplateId'], 

90 str(t.get('c7n:VersionAlias', t['VersionNumber']))): t['LaunchTemplateData'] 

91 for t in tmpl_mgr.get_resources(template_ids)} 

92 

93 def get_launch_configs(self, asgs): 

94 """Return a mapping of launch configs for the given set of asgs""" 

95 config_names = set() 

96 for a in asgs: 

97 if 'LaunchConfigurationName' not in a: 

98 continue 

99 config_names.add(a['LaunchConfigurationName']) 

100 if not config_names: 

101 return {} 

102 lc_resources = self.manager.get_resource_manager('launch-config') 

103 if len(config_names) < 5: 

104 configs = lc_resources.get_resources(list(config_names)) 

105 else: 

106 configs = lc_resources.resources() 

107 return { 

108 cfg['LaunchConfigurationName']: cfg for cfg in configs 

109 if cfg['LaunchConfigurationName'] in config_names} 

110 

111 def get_launch_id(self, asg): 

112 lid = asg.get('LaunchConfigurationName') 

113 if lid is not None: 

114 # We've noticed trailing white space allowed in some asgs 

115 return lid.strip() 

116 

117 lid = asg.get('LaunchTemplate') 

118 if lid is not None: 

119 return (lid['LaunchTemplateId'], lid.get('Version', '$Default')) 

120 

121 if 'MixedInstancesPolicy' in asg: 

122 mip_spec = asg['MixedInstancesPolicy'][ 

123 'LaunchTemplate']['LaunchTemplateSpecification'] 

124 return (mip_spec['LaunchTemplateId'], mip_spec.get('Version', '$Default')) 

125 

126 # we've noticed some corner cases where the asg name is the lc name, but not 

127 # explicitly specified as launchconfiguration attribute. 

128 lid = asg['AutoScalingGroupName'] 

129 return lid 

130 

131 def get(self, asg): 

132 lid = self.get_launch_id(asg) 

133 if isinstance(lid, tuple): 

134 return self.templates.get(lid) 

135 else: 

136 return self.configs.get(lid) 

137 

138 def items(self): 

139 return itertools.chain(*( 

140 self.configs.items(), self.templates.items())) 

141 

142 def get_image_ids(self): 

143 image_ids = {} 

144 for cid, c in self.items(): 

145 if c.get('ImageId'): 

146 image_ids.setdefault(c['ImageId'], []).append(cid) 

147 return image_ids 

148 

149 def get_image_map(self): 

150 # The describe_images api historically would return errors 

151 # on an unknown ami in the set of images ids passed in. 

152 # It now just silently drops those items, which is actually 

153 # ideally for our use case. 

154 # 

155 # We used to do some balancing of picking up our asgs using 

156 # the resource manager abstraction to take advantage of 

157 # resource caching, but then we needed to do separate api 

158 # calls to intersect with third party amis. Given the new 

159 # describe behavior, we'll just do the api call to fetch the 

160 # amis, it doesn't seem to have any upper bound on number of 

161 # ImageIds to pass (Tested with 1k+ ImageIds) 

162 # 

163 # Explicitly use a describe source. Can't use a config source 

164 # since it won't have state for third party ami, we auto 

165 # propagate source normally. Can't use a cache either as their 

166 # not in the account. 

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

168 self.manager.get_resource_manager( 

169 'ami').get_source('describe').get_resources( 

170 list(self.get_image_ids()), cache=False)} 

171 

172 def get_security_group_ids(self): 

173 # return set of security group ids for given asg 

174 sg_ids = set() 

175 for k, v in self.items(): 

176 sg_ids.update(v.get('SecurityGroupIds', ())) 

177 sg_ids.update(v.get('SecurityGroups', ())) 

178 return sg_ids 

179 

180 

181@ASG.filter_registry.register('security-group') 

182class SecurityGroupFilter(net_filters.SecurityGroupFilter): 

183 

184 RelatedIdsExpression = "" 

185 

186 permissions = ('ec2:DescribeSecurityGroups',) + LaunchInfo.permissions 

187 

188 def get_related_ids(self, asgs): 

189 return self.launch_info.get_security_group_ids() 

190 

191 def process(self, asgs, event=None): 

192 self.launch_info = LaunchInfo(self.manager).initialize(asgs) 

193 return super(SecurityGroupFilter, self).process(asgs, event) 

194 

195 

196@ASG.filter_registry.register('subnet') 

197class SubnetFilter(net_filters.SubnetFilter): 

198 

199 RelatedIdsExpression = "" 

200 

201 def get_related_ids(self, asgs): 

202 subnet_ids = set() 

203 for asg in asgs: 

204 subnet_ids.update( 

205 [sid.strip() for sid in asg.get('VPCZoneIdentifier', '').split(',')]) 

206 return subnet_ids 

207 

208 

209@ASG.filter_registry.register('launch-config') 

210class LaunchConfigFilter(ValueFilter): 

211 """Filter asg by launch config attributes. 

212 

213 This will also filter to launch template data in addition 

214 to launch configurations. 

215 

216 :example: 

217 

218 .. code-block:: yaml 

219 

220 policies: 

221 - name: launch-configs-with-public-address 

222 resource: asg 

223 filters: 

224 - type: launch-config 

225 key: AssociatePublicIpAddress 

226 value: true 

227 """ 

228 schema = type_schema( 

229 'launch-config', rinherit=ValueFilter.schema) 

230 schema_alias = False 

231 permissions = ("autoscaling:DescribeLaunchConfigurations",) 

232 

233 def process(self, asgs, event=None): 

234 self.launch_info = LaunchInfo(self.manager).initialize(asgs) 

235 return super(LaunchConfigFilter, self).process(asgs, event) 

236 

237 def __call__(self, asg): 

238 return self.match(self.launch_info.get(asg)) 

239 

240 

241class ConfigValidFilter(Filter): 

242 

243 def get_permissions(self): 

244 return list(itertools.chain(*[ 

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

246 for m in ('subnet', 'security-group', 'key-pair', 'elb', 

247 'app-elb-target-group', 'ebs-snapshot', 'ami')])) 

248 

249 def validate(self): 

250 if isinstance(self.manager.ctx.policy.get_execution_mode(), c7n.policy.LambdaMode): 

251 raise PolicyValidationError( 

252 "invalid-config makes too many queries to be run in lambda") 

253 return self 

254 

255 def initialize(self, asgs): 

256 self.launch_info = LaunchInfo(self.manager).initialize(asgs) 

257 # pylint: disable=attribute-defined-outside-init 

258 self.subnets, self.default_subnets = self.get_subnets() 

259 self.security_groups = self.get_security_groups() 

260 self.key_pairs = self.get_key_pairs() 

261 self.elbs = self.get_elbs() 

262 self.appelb_target_groups = self.get_appelb_target_groups() 

263 self.snapshots = self.get_snapshots() 

264 self.images, self.image_snaps = self.get_images() 

265 

266 def get_subnets(self): 

267 manager = self.manager.get_resource_manager('subnet') 

268 subnets = manager.resources() 

269 default_subnets = {s['SubnetId']: s['AvailabilityZone'] 

270 for s in subnets if s['DefaultForAz']} 

271 return {s['SubnetId'] for s in subnets}, default_subnets 

272 

273 def get_security_groups(self): 

274 manager = self.manager.get_resource_manager('security-group') 

275 return {s['GroupId'] for s in manager.resources()} 

276 

277 def get_key_pairs(self): 

278 manager = self.manager.get_resource_manager('key-pair') 

279 return {k['KeyName'] for k in manager.resources()} 

280 

281 def get_elbs(self): 

282 manager = self.manager.get_resource_manager('elb') 

283 return {e['LoadBalancerName'] for e in manager.resources()} 

284 

285 def get_appelb_target_groups(self): 

286 manager = self.manager.get_resource_manager('app-elb-target-group') 

287 return {a['TargetGroupArn'] for a in manager.resources()} 

288 

289 def get_images(self): 

290 images = self.launch_info.get_image_map() 

291 image_snaps = set() 

292 

293 for a in images.values(): 

294 # Capture any snapshots, images strongly reference their 

295 # snapshots, and some of these will be third party in the 

296 # case of a third party image. 

297 for bd in a.get('BlockDeviceMappings', ()): 

298 if 'Ebs' not in bd or 'SnapshotId' not in bd['Ebs']: 

299 continue 

300 image_snaps.add(bd['Ebs']['SnapshotId'].strip()) 

301 return set(images), image_snaps 

302 

303 def get_snapshots(self): 

304 snaps = set() 

305 for cid, cfg in self.launch_info.items(): 

306 for bd in cfg.get('BlockDeviceMappings', ()): 

307 if 'Ebs' not in bd or 'SnapshotId' not in bd['Ebs']: 

308 continue 

309 snaps.add(bd['Ebs']['SnapshotId'].strip()) 

310 manager = self.manager.get_resource_manager('ebs-snapshot') 

311 return {s['SnapshotId'] for s in manager.get_resources( 

312 list(snaps), cache=False)} 

313 

314 def process(self, asgs, event=None): 

315 self.initialize(asgs) 

316 return super(ConfigValidFilter, self).process(asgs, event) 

317 

318 def get_asg_errors(self, asg): 

319 errors = [] 

320 cfg_id = self.launch_info.get_launch_id(asg) 

321 cfg = self.launch_info.get(asg) 

322 

323 subnets = asg.get('VPCZoneIdentifier', '').split(',') 

324 

325 if subnets[0]: 

326 for subnet in subnets: 

327 subnet = subnet.strip() 

328 if subnet not in self.subnets: 

329 errors.append(('invalid-subnet', subnet)) 

330 else: 

331 if 'NetworkInterfaces' not in cfg: 

332 for az in asg.get('AvailabilityZones', []): 

333 if az not in self.default_subnets.values(): 

334 errors.append(('invalid-availability-zone', az)) 

335 

336 for elb in asg['LoadBalancerNames']: 

337 elb = elb.strip() 

338 if elb not in self.elbs: 

339 errors.append(('invalid-elb', elb)) 

340 

341 for appelb_target in asg.get('TargetGroupARNs', []): 

342 appelb_target = appelb_target.strip() 

343 if appelb_target not in self.appelb_target_groups: 

344 errors.append(('invalid-appelb-target-group', appelb_target)) 

345 

346 if cfg is None: 

347 errors.append(('invalid-config', cfg_id)) 

348 self.log.debug( 

349 "asg:%s no launch config or template found" % asg['AutoScalingGroupName']) 

350 asg['Invalid'] = errors 

351 return True 

352 

353 for sg in itertools.chain(*( 

354 cfg.get('SecurityGroups', ()), cfg.get('SecurityGroupIds', ()))): 

355 sg = sg.strip() 

356 if sg not in self.security_groups: 

357 errors.append(('invalid-security-group', sg)) 

358 

359 if cfg.get('KeyName') and cfg['KeyName'].strip() not in self.key_pairs: 

360 errors.append(('invalid-key-pair', cfg['KeyName'])) 

361 

362 if cfg.get('ImageId') and cfg['ImageId'].strip() not in self.images: 

363 errors.append(('invalid-image', cfg['ImageId'])) 

364 

365 for bd in cfg.get('BlockDeviceMappings', ()): 

366 if 'Ebs' not in bd or 'SnapshotId' not in bd['Ebs']: 

367 continue 

368 snapshot_id = bd['Ebs']['SnapshotId'].strip() 

369 if snapshot_id in self.image_snaps: 

370 continue 

371 if snapshot_id not in self.snapshots: 

372 errors.append(('invalid-snapshot', bd['Ebs']['SnapshotId'])) 

373 return errors 

374 

375 

376@ASG.filter_registry.register('valid') 

377class ValidConfigFilter(ConfigValidFilter): 

378 """Filters autoscale groups to find those that are structurally valid. 

379 

380 This operates as the inverse of the invalid filter for multi-step 

381 workflows. 

382 

383 See details on the invalid filter for a list of checks made. 

384 

385 :example: 

386 

387 .. code-block:: yaml 

388 

389 policies: 

390 - name: asg-valid-config 

391 resource: asg 

392 filters: 

393 - valid 

394 """ 

395 

396 schema = type_schema('valid') 

397 

398 def __call__(self, asg): 

399 errors = self.get_asg_errors(asg) 

400 return not bool(errors) 

401 

402 

403@ASG.filter_registry.register('invalid') 

404class InvalidConfigFilter(ConfigValidFilter): 

405 """Filter autoscale groups to find those that are structurally invalid. 

406 

407 Structurally invalid means that the auto scale group will not be able 

408 to launch an instance succesfully as the configuration has 

409 

410 - invalid subnets 

411 - invalid security groups 

412 - invalid key pair name 

413 - invalid launch config volume snapshots 

414 - invalid amis 

415 - invalid health check elb (slower) 

416 

417 Internally this tries to reuse other resource managers for better 

418 cache utilization. 

419 

420 :example: 

421 

422 .. code-block:: yaml 

423 

424 policies: 

425 - name: asg-invalid-config 

426 resource: asg 

427 filters: 

428 - invalid 

429 """ 

430 schema = type_schema('invalid') 

431 

432 def __call__(self, asg): 

433 errors = self.get_asg_errors(asg) 

434 if errors: 

435 asg['Invalid'] = errors 

436 return True 

437 

438 

439@ASG.filter_registry.register('not-encrypted') 

440class NotEncryptedFilter(Filter): 

441 """Check if an ASG is configured to have unencrypted volumes. 

442 

443 Checks both the ami snapshots and the launch configuration. 

444 

445 :example: 

446 

447 .. code-block:: yaml 

448 

449 policies: 

450 - name: asg-unencrypted 

451 resource: asg 

452 filters: 

453 - type: not-encrypted 

454 exclude_image: true 

455 """ 

456 

457 schema = type_schema('not-encrypted', exclude_image={'type': 'boolean'}) 

458 permissions = ( 

459 'ec2:DescribeImages', 

460 'ec2:DescribeSnapshots', 

461 'autoscaling:DescribeLaunchConfigurations') 

462 

463 images = unencrypted_configs = unencrypted_images = None 

464 

465 # TODO: resource-manager, notfound err mgr 

466 

467 def process(self, asgs, event=None): 

468 self.launch_info = LaunchInfo(self.manager).initialize(asgs) 

469 self.images = self.launch_info.get_image_map() 

470 

471 if not self.data.get('exclude_image'): 

472 self.unencrypted_images = self.get_unencrypted_images() 

473 

474 self.unencrypted_launch = self.get_unencrypted_configs() 

475 return super(NotEncryptedFilter, self).process(asgs, event) 

476 

477 def __call__(self, asg): 

478 launch = self.launch_info.get(asg) 

479 if not launch: 

480 self.log.warning( 

481 "ASG %s instances: %d has missing config or template", 

482 asg['AutoScalingGroupName'], len(asg['Instances'])) 

483 return False 

484 

485 launch_id = self.launch_info.get_launch_id(asg) 

486 unencrypted = [] 

487 if not self.data.get('exclude_image'): 

488 if launch['ImageId'] in self.unencrypted_images: 

489 unencrypted.append('Image') 

490 

491 if launch_id in self.unencrypted_launch: 

492 unencrypted.append('LaunchConfig') 

493 if unencrypted: 

494 asg['Unencrypted'] = unencrypted 

495 return bool(unencrypted) 

496 

497 def get_unencrypted_images(self): 

498 """retrieve images which have unencrypted snapshots referenced.""" 

499 unencrypted_images = set() 

500 for i in self.images.values(): 

501 for bd in i['BlockDeviceMappings']: 

502 if 'Ebs' in bd and not bd['Ebs'].get('Encrypted'): 

503 unencrypted_images.add(i['ImageId']) 

504 break 

505 return unencrypted_images 

506 

507 def get_unencrypted_configs(self): 

508 """retrieve configs that have unencrypted ebs voluems referenced.""" 

509 unencrypted_configs = set() 

510 snaps = {} 

511 

512 for cid, c in self.launch_info.items(): 

513 image = self.images.get(c.get('ImageId', '')) 

514 # image deregistered/unavailable or exclude_image set 

515 if image is not None: 

516 image_block_devs = { 

517 bd['DeviceName'] for bd in 

518 image['BlockDeviceMappings'] if 'Ebs' in bd} 

519 else: 

520 image_block_devs = set() 

521 for bd in c.get('BlockDeviceMappings', ()): 

522 if 'Ebs' not in bd: 

523 continue 

524 # Launch configs can shadow image devices, images have 

525 # precedence. 

526 if bd['DeviceName'] in image_block_devs: 

527 continue 

528 if 'SnapshotId' in bd['Ebs']: 

529 snaps.setdefault( 

530 bd['Ebs']['SnapshotId'].strip(), []).append(cid) 

531 elif not bd['Ebs'].get('Encrypted'): 

532 unencrypted_configs.add(cid) 

533 if not snaps: 

534 return unencrypted_configs 

535 

536 for s in self.get_snapshots(list(snaps.keys())): 

537 if not s.get('Encrypted'): 

538 unencrypted_configs.update(snaps[s['SnapshotId']]) 

539 return unencrypted_configs 

540 

541 def get_snapshots(self, snap_ids): 

542 """get snapshots corresponding to id, but tolerant of invalid id's.""" 

543 return self.manager.get_resource_manager('ebs-snapshot').get_resources( 

544 snap_ids, cache=False) 

545 

546 

547@ASG.filter_registry.register('image-age') 

548class ImageAgeFilter(AgeFilter): 

549 """Filter asg by image age (in days). 

550 

551 :example: 

552 

553 .. code-block:: yaml 

554 

555 policies: 

556 - name: asg-older-image 

557 resource: asg 

558 filters: 

559 - type: image-age 

560 days: 90 

561 op: ge 

562 """ 

563 permissions = ( 

564 "ec2:DescribeImages", 

565 "autoscaling:DescribeLaunchConfigurations") 

566 

567 date_attribute = "CreationDate" 

568 schema = type_schema( 

569 'image-age', 

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

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

572 

573 def process(self, asgs, event=None): 

574 self.launch_info = LaunchInfo(self.manager).initialize(asgs) 

575 self.images = self.launch_info.get_image_map() 

576 return super(ImageAgeFilter, self).process(asgs, event) 

577 

578 def get_resource_date(self, asg): 

579 cfg = self.launch_info.get(asg) 

580 if cfg is None: 

581 cfg = {} 

582 ami = self.images.get(cfg.get('ImageId'), {}) 

583 return parse(ami.get( 

584 self.date_attribute, "2000-01-01T01:01:01.000Z")) 

585 

586 

587@ASG.filter_registry.register('image') 

588class ImageFilter(ValueFilter): 

589 """Filter asg by image 

590 

591 :example: 

592 

593 .. code-block:: yaml 

594 

595 policies: 

596 - name: non-windows-asg 

597 resource: asg 

598 filters: 

599 - type: image 

600 key: Platform 

601 value: Windows 

602 op: ne 

603 """ 

604 permissions = ( 

605 "ec2:DescribeImages", 

606 "autoscaling:DescribeLaunchConfigurations") 

607 

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

609 schema_alias = True 

610 

611 def process(self, asgs, event=None): 

612 self.launch_info = LaunchInfo(self.manager).initialize(asgs) 

613 self.images = self.launch_info.get_image_map() 

614 return super(ImageFilter, self).process(asgs, event) 

615 

616 def __call__(self, i): 

617 image_id = self.launch_info.get(i).get('ImageId', None) 

618 image = self.images.get(image_id) 

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

620 if not image: 

621 self.log.warning( 

622 "Could not locate image for asg:%s ami:%s" % ( 

623 i['AutoScalingGroupName'], image_id)) 

624 # Match instead on empty skeleton? 

625 return False 

626 return self.match(image) 

627 

628 

629@ASG.filter_registry.register('vpc-id') 

630class VpcIdFilter(ValueFilter): 

631 """Filters ASG based on the VpcId 

632 

633 This filter is available as a ValueFilter as the vpc-id is not natively 

634 associated to the results from describing the autoscaling groups. 

635 

636 :example: 

637 

638 .. code-block:: yaml 

639 

640 policies: 

641 - name: asg-vpc-xyz 

642 resource: asg 

643 filters: 

644 - type: vpc-id 

645 value: vpc-12ab34cd 

646 """ 

647 

648 schema = type_schema( 

649 'vpc-id', rinherit=ValueFilter.schema) 

650 schema['properties'].pop('key') 

651 schema_alias = False 

652 permissions = ('ec2:DescribeSubnets',) 

653 

654 # TODO: annotation 

655 

656 def __init__(self, data, manager=None): 

657 super(VpcIdFilter, self).__init__(data, manager) 

658 self.data['key'] = 'VpcId' 

659 

660 def process(self, asgs, event=None): 

661 subnets = {} 

662 for a in asgs: 

663 subnet_ids = a.get('VPCZoneIdentifier', '') 

664 if not subnet_ids: 

665 continue 

666 subnets.setdefault(subnet_ids.split(',')[0], []).append(a) 

667 

668 subnet_manager = self.manager.get_resource_manager('subnet') 

669 # Invalid subnets on asgs happen, so query all 

670 all_subnets = {s['SubnetId']: s for s in subnet_manager.resources()} 

671 

672 for s, s_asgs in subnets.items(): 

673 if s not in all_subnets: 

674 self.log.warning( 

675 "invalid subnet %s for asgs: %s", 

676 s, [a['AutoScalingGroupName'] for a in s_asgs]) 

677 continue 

678 for a in s_asgs: 

679 a['VpcId'] = all_subnets[s]['VpcId'] 

680 return super(VpcIdFilter, self).process(asgs) 

681 

682 

683@ASG.filter_registry.register('progagated-tags') # compatibility 

684@ASG.filter_registry.register('propagated-tags') 

685class PropagatedTagFilter(Filter): 

686 """Filter ASG based on propagated tags 

687 

688 This filter is designed to find all autoscaling groups that have a list 

689 of tag keys (provided) that are set to propagate to new instances. Using 

690 this will allow for easy validation of asg tag sets are in place across an 

691 account for compliance. 

692 

693 :example: 

694 

695 .. code-block:: yaml 

696 

697 policies: 

698 - name: asg-non-propagated-tags 

699 resource: asg 

700 filters: 

701 - type: propagated-tags 

702 keys: ["ABC", "BCD"] 

703 match: false 

704 propagate: true 

705 """ 

706 schema = type_schema( 

707 'progagated-tags', 

708 aliases=('propagated-tags',), 

709 keys={'type': 'array', 'items': {'type': 'string'}}, 

710 match={'type': 'boolean'}, 

711 propagate={'type': 'boolean'}) 

712 permissions = ( 

713 "autoscaling:DescribeLaunchConfigurations", 

714 "autoscaling:DescribeAutoScalingGroups") 

715 

716 def process(self, asgs, event=None): 

717 keys = self.data.get('keys', []) 

718 match = self.data.get('match', True) 

719 results = [] 

720 for asg in asgs: 

721 if self.data.get('propagate', True): 

722 tags = [t['Key'] for t in asg.get('Tags', []) if t[ 

723 'Key'] in keys and t['PropagateAtLaunch']] 

724 if match and all(k in tags for k in keys): 

725 results.append(asg) 

726 if not match and not all(k in tags for k in keys): 

727 results.append(asg) 

728 else: 

729 tags = [t['Key'] for t in asg.get('Tags', []) if t[ 

730 'Key'] in keys and not t['PropagateAtLaunch']] 

731 if match and all(k in tags for k in keys): 

732 results.append(asg) 

733 if not match and not all(k in tags for k in keys): 

734 results.append(asg) 

735 return results 

736 

737 

738@ASG.action_registry.register('post-finding') 

739class AsgPostFinding(PostFinding): 

740 

741 resource_type = 'AwsAutoScalingAutoScalingGroup' 

742 launch_info = LaunchInfo(None) 

743 

744 def format_resource(self, r): 

745 envelope, payload = self.format_envelope(r) 

746 details = select_keys(r, [ 

747 'CreatedTime', 'HealthCheckType', 'HealthCheckGracePeriod', 'LoadBalancerNames']) 

748 lid = self.launch_info.get_launch_id(r) 

749 if isinstance(lid, tuple): 

750 lid = "%s:%s" % lid 

751 details['CreatedTime'] = details['CreatedTime'].isoformat() 

752 # let's arbitrarily cut off key information per security hub's restrictions... 

753 details['LaunchConfigurationName'] = lid[:32] 

754 payload.update(details) 

755 return envelope 

756 

757 

758@ASG.action_registry.register('auto-tag-user') 

759class AutoScaleAutoTagUser(AutoTagUser): 

760 

761 schema = type_schema( 

762 'auto-tag-user', 

763 propagate={'type': 'boolean'}, 

764 rinherit=AutoTagUser.schema) 

765 schema_alias = False 

766 

767 def set_resource_tags(self, tags, resources): 

768 tag_action = self.manager.action_registry.get('tag') 

769 tag_action( 

770 {'tags': tags, 'propagate': self.data.get('propagate', False)}, 

771 self.manager).process(resources) 

772 

773 

774@ASG.action_registry.register('tag-trim') 

775class GroupTagTrim(TagTrim): 

776 """Action to trim the number of tags to avoid hitting tag limits 

777 

778 :example: 

779 

780 .. code-block:: yaml 

781 

782 policies: 

783 - name: asg-tag-trim 

784 resource: asg 

785 filters: 

786 - type: tag-count 

787 count: 10 

788 actions: 

789 - type: tag-trim 

790 space: 1 

791 preserve: 

792 - OwnerName 

793 - OwnerContact 

794 """ 

795 

796 max_tag_count = 10 

797 permissions = ('autoscaling:DeleteTags',) 

798 

799 def process_tag_removal(self, client, resource, candidates): 

800 tags = [] 

801 for t in candidates: 

802 tags.append( 

803 dict(Key=t, ResourceType='auto-scaling-group', 

804 ResourceId=resource['AutoScalingGroupName'])) 

805 client.delete_tags(Tags=tags) 

806 

807 

808@ASG.filter_registry.register('capacity-delta') 

809class CapacityDelta(Filter): 

810 """Filter returns ASG that have less instances than desired or required 

811 

812 :example: 

813 

814 .. code-block:: yaml 

815 

816 policies: 

817 - name: asg-capacity-delta 

818 resource: asg 

819 filters: 

820 - capacity-delta 

821 """ 

822 

823 schema = type_schema('capacity-delta') 

824 

825 def process(self, asgs, event=None): 

826 return [ 

827 a for a in asgs if len( 

828 a['Instances']) < a['DesiredCapacity'] or len( 

829 a['Instances']) < a['MinSize']] 

830 

831 

832@ASG.filter_registry.register('user-data') 

833class UserDataFilter(ValueFilter): 

834 """Filter on ASG's whose launch configs have matching userdata. 

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

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

837 

838 :example: 

839 

840 .. code-block:: yaml 

841 

842 policies: 

843 - name: lc_userdata 

844 resource: asg 

845 filters: 

846 - type: user-data 

847 op: regex 

848 value: (?smi).*password= 

849 actions: 

850 - delete 

851 """ 

852 

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

854 schema_alias = False 

855 batch_size = 50 

856 annotation = 'c7n:user-data' 

857 

858 def __init__(self, data, manager): 

859 super(UserDataFilter, self).__init__(data, manager) 

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

861 

862 def get_permissions(self): 

863 return self.manager.get_resource_manager('asg').get_permissions() 

864 

865 def process(self, asgs, event=None): 

866 '''Get list of autoscaling groups whose launch configs match the 

867 user-data filter. 

868 

869 :return: List of ASG's with matching launch configs 

870 ''' 

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

872 launch_info = LaunchInfo(self.manager).initialize(asgs) 

873 

874 results = [] 

875 for asg in asgs: 

876 launch_config = launch_info.get(asg) 

877 if self.annotation not in launch_config: 

878 if not launch_config.get('UserData'): 

879 asg[self.annotation] = None 

880 else: 

881 asg[self.annotation] = deserialize_user_data( 

882 launch_config['UserData']) 

883 if self.match(asg): 

884 results.append(asg) 

885 return results 

886 

887 

888@ASG.action_registry.register('resize') 

889class Resize(Action): 

890 """Action to resize the min/max/desired instances in an ASG 

891 

892 There are several ways to use this action: 

893 

894 1. set min/desired to current running instances 

895 

896 .. code-block:: yaml 

897 

898 policies: 

899 - name: asg-resize 

900 resource: asg 

901 filters: 

902 - capacity-delta 

903 actions: 

904 - type: resize 

905 desired-size: "current" 

906 

907 2. apply a fixed resize of min, max or desired, optionally saving the 

908 previous values to a named tag (for restoring later): 

909 

910 .. code-block:: yaml 

911 

912 policies: 

913 - name: offhours-asg-off 

914 resource: asg 

915 filters: 

916 - type: offhour 

917 offhour: 19 

918 default_tz: bst 

919 actions: 

920 - type: resize 

921 min-size: 0 

922 desired-size: 0 

923 save-options-tag: OffHoursPrevious 

924 

925 3. restore previous values for min/max/desired from a tag: 

926 

927 .. code-block:: yaml 

928 

929 policies: 

930 - name: offhours-asg-on 

931 resource: asg 

932 filters: 

933 - type: onhour 

934 onhour: 8 

935 default_tz: bst 

936 actions: 

937 - type: resize 

938 restore-options-tag: OffHoursPrevious 

939 

940 """ 

941 

942 schema = type_schema( 

943 'resize', 

944 **{ 

945 'min-size': {'type': 'integer', 'minimum': 0}, 

946 'max-size': {'type': 'integer', 'minimum': 0}, 

947 'desired-size': { 

948 "anyOf": [ 

949 {'enum': ["current"]}, 

950 {'type': 'integer', 'minimum': 0} 

951 ] 

952 }, 

953 # support previous key name with underscore 

954 'desired_size': { 

955 "anyOf": [ 

956 {'enum': ["current"]}, 

957 {'type': 'integer', 'minimum': 0} 

958 ] 

959 }, 

960 'save-options-tag': {'type': 'string'}, 

961 'restore-options-tag': {'type': 'string'}, 

962 } 

963 ) 

964 permissions = ( 

965 'autoscaling:UpdateAutoScalingGroup', 

966 'autoscaling:CreateOrUpdateTags' 

967 ) 

968 

969 def process(self, asgs): 

970 # ASG parameters to save to/restore from a tag 

971 asg_params = ['MinSize', 'MaxSize', 'DesiredCapacity'] 

972 

973 # support previous param desired_size when desired-size is not present 

974 if 'desired_size' in self.data and 'desired-size' not in self.data: 

975 self.data['desired-size'] = self.data['desired_size'] 

976 

977 client = local_session(self.manager.session_factory).client( 

978 'autoscaling') 

979 for a in asgs: 

980 tag_map = {t['Key']: t['Value'] for t in a.get('Tags', [])} 

981 update = {} 

982 current_size = len(a['Instances']) 

983 

984 if 'restore-options-tag' in self.data: 

985 # we want to restore all ASG size params from saved data 

986 self.log.debug( 

987 'Want to restore ASG %s size from tag %s' % 

988 (a['AutoScalingGroupName'], self.data['restore-options-tag'])) 

989 if self.data['restore-options-tag'] in tag_map: 

990 for field in tag_map[self.data['restore-options-tag']].split(';'): 

991 (param, value) = field.split('=') 

992 if param in asg_params: 

993 update[param] = int(value) 

994 

995 else: 

996 # we want to resize, parse provided params 

997 if 'min-size' in self.data: 

998 update['MinSize'] = self.data['min-size'] 

999 

1000 if 'max-size' in self.data: 

1001 update['MaxSize'] = self.data['max-size'] 

1002 

1003 if 'desired-size' in self.data: 

1004 if self.data['desired-size'] == 'current': 

1005 update['DesiredCapacity'] = min(current_size, a['DesiredCapacity']) 

1006 if 'MinSize' not in update: 

1007 # unless we were given a new value for min_size then 

1008 # ensure it is at least as low as current_size 

1009 update['MinSize'] = min(current_size, a['MinSize']) 

1010 elif isinstance(self.data['desired-size'], int): 

1011 update['DesiredCapacity'] = self.data['desired-size'] 

1012 

1013 if update: 

1014 self.log.debug('ASG %s size: current=%d, min=%d, max=%d, desired=%d' 

1015 % (a['AutoScalingGroupName'], current_size, a['MinSize'], 

1016 a['MaxSize'], a['DesiredCapacity'])) 

1017 

1018 if 'save-options-tag' in self.data: 

1019 # save existing ASG params to a tag before changing them 

1020 self.log.debug('Saving ASG %s size to tag %s' % 

1021 (a['AutoScalingGroupName'], self.data['save-options-tag'])) 

1022 tags = [dict( 

1023 Key=self.data['save-options-tag'], 

1024 PropagateAtLaunch=False, 

1025 Value=';'.join({'%s=%d' % (param, a[param]) for param in asg_params}), 

1026 ResourceId=a['AutoScalingGroupName'], 

1027 ResourceType='auto-scaling-group', 

1028 )] 

1029 self.manager.retry(client.create_or_update_tags, Tags=tags) 

1030 

1031 self.log.debug('Resizing ASG %s with %s' % (a['AutoScalingGroupName'], 

1032 str(update))) 

1033 self.manager.retry( 

1034 client.update_auto_scaling_group, 

1035 AutoScalingGroupName=a['AutoScalingGroupName'], 

1036 **update) 

1037 else: 

1038 self.log.debug('nothing to resize') 

1039 

1040 

1041@ASG.action_registry.register('remove-tag') 

1042@ASG.action_registry.register('untag') # compatibility 

1043@ASG.action_registry.register('unmark') # compatibility 

1044class RemoveTag(Action): 

1045 """Action to remove tag/tags from an ASG 

1046 

1047 :example: 

1048 

1049 .. code-block:: yaml 

1050 

1051 policies: 

1052 - name: asg-remove-unnecessary-tags 

1053 resource: asg 

1054 filters: 

1055 - "tag:UnnecessaryTag": present 

1056 actions: 

1057 - type: remove-tag 

1058 key: UnnecessaryTag 

1059 """ 

1060 

1061 schema = type_schema( 

1062 'remove-tag', 

1063 aliases=('untag', 'unmark'), 

1064 tags={'type': 'array', 'items': {'type': 'string'}}, 

1065 key={'type': 'string'}) 

1066 

1067 permissions = ('autoscaling:DeleteTags',) 

1068 batch_size = 1 

1069 

1070 def process(self, asgs): 

1071 error = False 

1072 tags = self.data.get('tags', []) 

1073 if not tags: 

1074 tags = [self.data.get('key', DEFAULT_TAG)] 

1075 client = local_session(self.manager.session_factory).client('autoscaling') 

1076 

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

1078 futures = {} 

1079 for asg_set in chunks(asgs, self.batch_size): 

1080 futures[w.submit( 

1081 self.process_resource_set, client, asg_set, tags)] = asg_set 

1082 for f in as_completed(futures): 

1083 asg_set = futures[f] 

1084 if f.exception(): 

1085 error = f.exception() 

1086 self.log.exception( 

1087 "Exception untagging asg:%s tag:%s error:%s" % ( 

1088 ", ".join([a['AutoScalingGroupName'] 

1089 for a in asg_set]), 

1090 self.data.get('key', DEFAULT_TAG), 

1091 f.exception())) 

1092 if error: 

1093 raise error 

1094 

1095 def process_resource_set(self, client, asgs, tags): 

1096 tag_set = [] 

1097 for a in asgs: 

1098 for t in tags: 

1099 tag_set.append(dict( 

1100 Key=t, ResourceType='auto-scaling-group', 

1101 ResourceId=a['AutoScalingGroupName'])) 

1102 self.manager.retry(client.delete_tags, Tags=tag_set) 

1103 

1104 

1105@ASG.action_registry.register('tag') 

1106@ASG.action_registry.register('mark') 

1107class Tag(Action): 

1108 """Action to add a tag to an ASG 

1109 

1110 The *propagate* parameter can be used to specify that the tag being added 

1111 will need to be propagated down to each ASG instance associated or simply 

1112 to the ASG itself. 

1113 

1114 :example: 

1115 

1116 .. code-block:: yaml 

1117 

1118 policies: 

1119 - name: asg-add-owner-tag 

1120 resource: asg 

1121 filters: 

1122 - "tag:OwnerName": absent 

1123 actions: 

1124 - type: tag 

1125 key: OwnerName 

1126 value: OwnerName 

1127 propagate: true 

1128 """ 

1129 

1130 schema = type_schema( 

1131 'tag', 

1132 key={'type': 'string'}, 

1133 value={'type': 'string'}, 

1134 tags={'type': 'object'}, 

1135 # Backwards compatibility 

1136 tag={'type': 'string'}, 

1137 msg={'type': 'string'}, 

1138 propagate={'type': 'boolean'}, 

1139 aliases=('mark',) 

1140 ) 

1141 permissions = ('autoscaling:CreateOrUpdateTags',) 

1142 batch_size = 1 

1143 

1144 def get_tag_set(self): 

1145 tags = [] 

1146 key = self.data.get('key', self.data.get('tag', DEFAULT_TAG)) 

1147 value = self.data.get( 

1148 'value', self.data.get( 

1149 'msg', 'AutoScaleGroup does not meet policy guidelines')) 

1150 if key and value: 

1151 tags.append({'Key': key, 'Value': value}) 

1152 

1153 for k, v in self.data.get('tags', {}).items(): 

1154 tags.append({'Key': k, 'Value': v}) 

1155 

1156 return tags 

1157 

1158 def process(self, asgs): 

1159 tags = self.get_tag_set() 

1160 error = None 

1161 

1162 self.interpolate_values(tags) 

1163 

1164 client = self.get_client() 

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

1166 futures = {} 

1167 for asg_set in chunks(asgs, self.batch_size): 

1168 futures[w.submit( 

1169 self.process_resource_set, client, asg_set, tags)] = asg_set 

1170 for f in as_completed(futures): 

1171 asg_set = futures[f] 

1172 if f.exception(): 

1173 self.log.exception( 

1174 "Exception tagging tag:%s error:%s asg:%s" % ( 

1175 tags, 

1176 f.exception(), 

1177 ", ".join([a['AutoScalingGroupName'] 

1178 for a in asg_set]))) 

1179 if error: 

1180 raise error 

1181 

1182 def process_resource_set(self, client, asgs, tags): 

1183 tag_params = [] 

1184 propagate = self.data.get('propagate', False) 

1185 for t in tags: 

1186 if 'PropagateAtLaunch' not in t: 

1187 t['PropagateAtLaunch'] = propagate 

1188 for t in tags: 

1189 for a in asgs: 

1190 atags = dict(t) 

1191 atags['ResourceType'] = 'auto-scaling-group' 

1192 atags['ResourceId'] = a['AutoScalingGroupName'] 

1193 tag_params.append(atags) 

1194 a.setdefault('Tags', []).append(atags) 

1195 self.manager.retry(client.create_or_update_tags, Tags=tag_params) 

1196 

1197 def interpolate_values(self, tags): 

1198 params = { 

1199 'account_id': self.manager.config.account_id, 

1200 'now': FormatDate.utcnow(), 

1201 'region': self.manager.config.region} 

1202 for t in tags: 

1203 t['Value'] = t['Value'].format(**params) 

1204 

1205 def get_client(self): 

1206 return local_session(self.manager.session_factory).client('autoscaling') 

1207 

1208 

1209@ASG.action_registry.register('propagate-tags') 

1210class PropagateTags(Action): 

1211 """Propagate tags to an asg instances. 

1212 

1213 In AWS changing an asg tag does not automatically propagate to 

1214 extant instances even if the tag is set to propagate. It only 

1215 is applied to new instances. 

1216 

1217 This action exists to ensure that extant instances also have these 

1218 propagated tags set, and can also trim older tags not present on 

1219 the asg anymore that are present on instances. 

1220 

1221 :example: 

1222 

1223 .. code-block:: yaml 

1224 

1225 policies: 

1226 - name: asg-propagate-required 

1227 resource: asg 

1228 filters: 

1229 - "tag:OwnerName": present 

1230 actions: 

1231 - type: propagate-tags 

1232 tags: 

1233 - OwnerName 

1234 

1235 """ 

1236 

1237 schema = type_schema( 

1238 'propagate-tags', 

1239 tags={'type': 'array', 'items': {'type': 'string'}}, 

1240 trim={'type': 'boolean'}) 

1241 permissions = ('ec2:DeleteTags', 'ec2:CreateTags') 

1242 

1243 def validate(self): 

1244 if not isinstance(self.data.get('tags', []), (list, tuple)): 

1245 raise ValueError("No tags specified") 

1246 return self 

1247 

1248 def process(self, asgs): 

1249 if not asgs: 

1250 return 

1251 if self.data.get('trim', False): 

1252 self.instance_map = self.get_instance_map(asgs) 

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

1254 instance_count = sum(list(w.map(self.process_asg, asgs))) 

1255 self.log.info("Applied tags to %d instances" % instance_count) 

1256 

1257 def process_asg(self, asg): 

1258 instance_ids = [i['InstanceId'] for i in asg['Instances']] 

1259 tag_map = {t['Key']: t['Value'] for t in asg.get('Tags', []) 

1260 if t['PropagateAtLaunch'] and not t['Key'].startswith('aws:')} 

1261 

1262 if self.data.get('tags'): 

1263 tag_map = { 

1264 k: v for k, v in tag_map.items() 

1265 if k in self.data['tags']} 

1266 

1267 if not tag_map and not self.data.get('trim', False): 

1268 self.log.error( 

1269 'No tags found to propagate on asg:{} tags configured:{}'.format( 

1270 asg['AutoScalingGroupName'], self.data.get('tags'))) 

1271 

1272 tag_set = set(tag_map) 

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

1274 

1275 if self.data.get('trim', False): 

1276 instances = [self.instance_map[i] for i in instance_ids] 

1277 self.prune_instance_tags(client, asg, tag_set, instances) 

1278 

1279 if not self.manager.config.dryrun and instance_ids and tag_map: 

1280 client.create_tags( 

1281 Resources=instance_ids, 

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

1283 return len(instance_ids) 

1284 

1285 def prune_instance_tags(self, client, asg, tag_set, instances): 

1286 """Remove tags present on all asg instances which are not present 

1287 on the asg. 

1288 """ 

1289 instance_tags = Counter() 

1290 instance_count = len(instances) 

1291 

1292 remove_tags = [] 

1293 extra_tags = [] 

1294 

1295 for i in instances: 

1296 instance_tags.update([ 

1297 t['Key'] for t in i['Tags'] 

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

1299 for k, v in instance_tags.items(): 

1300 if not v >= instance_count: 

1301 extra_tags.append(k) 

1302 continue 

1303 if k not in tag_set: 

1304 remove_tags.append(k) 

1305 

1306 if remove_tags: 

1307 self.log.debug("Pruning asg:%s instances:%d of old tags: %s" % ( 

1308 asg['AutoScalingGroupName'], instance_count, remove_tags)) 

1309 if extra_tags: 

1310 self.log.debug("Asg: %s has uneven tags population: %s" % ( 

1311 asg['AutoScalingGroupName'], instance_tags)) 

1312 # Remove orphan tags 

1313 remove_tags.extend(extra_tags) 

1314 

1315 if not self.manager.config.dryrun: 

1316 client.delete_tags( 

1317 Resources=[i['InstanceId'] for i in instances], 

1318 Tags=[{'Key': t} for t in remove_tags]) 

1319 

1320 def get_instance_map(self, asgs): 

1321 instance_ids = [ 

1322 i['InstanceId'] for i in 

1323 list(itertools.chain(*[ 

1324 g['Instances'] 

1325 for g in asgs if g['Instances']]))] 

1326 if not instance_ids: 

1327 return {} 

1328 return {i['InstanceId']: i for i in 

1329 self.manager.get_resource_manager( 

1330 'ec2').get_resources(instance_ids)} 

1331 

1332 

1333@ASG.action_registry.register('rename-tag') 

1334class RenameTag(Action): 

1335 """Rename a tag on an AutoScaleGroup. 

1336 

1337 :example: 

1338 

1339 .. code-block:: yaml 

1340 

1341 policies: 

1342 - name: asg-rename-owner-tag 

1343 resource: asg 

1344 filters: 

1345 - "tag:OwnerNames": present 

1346 actions: 

1347 - type: rename-tag 

1348 propagate: true 

1349 source: OwnerNames 

1350 dest: OwnerName 

1351 """ 

1352 

1353 schema = type_schema( 

1354 'rename-tag', required=['source', 'dest'], 

1355 propagate={'type': 'boolean'}, 

1356 source={'type': 'string'}, 

1357 dest={'type': 'string'}) 

1358 

1359 def get_permissions(self): 

1360 permissions = ( 

1361 'autoscaling:CreateOrUpdateTags', 

1362 'autoscaling:DeleteTags') 

1363 if self.data.get('propagate', True): 

1364 permissions += ('ec2:CreateTags', 'ec2:DeleteTags') 

1365 return permissions 

1366 

1367 def process(self, asgs): 

1368 source = self.data.get('source') 

1369 dest = self.data.get('dest') 

1370 count = len(asgs) 

1371 

1372 filtered = [] 

1373 for a in asgs: 

1374 for t in a.get('Tags'): 

1375 if t['Key'] == source: 

1376 filtered.append(a) 

1377 break 

1378 asgs = filtered 

1379 self.log.info("Filtered from %d asgs to %d", count, len(asgs)) 

1380 self.log.info( 

1381 "Renaming %s to %s on %d asgs", source, dest, len(filtered)) 

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

1383 list(w.map(self.process_asg, asgs)) 

1384 

1385 def process_asg(self, asg): 

1386 """Move source tag to destination tag. 

1387 

1388 Check tag count on asg 

1389 Create new tag tag 

1390 Delete old tag 

1391 Check tag count on instance 

1392 Create new tag 

1393 Delete old tag 

1394 """ 

1395 source_tag = self.data.get('source') 

1396 tag_map = {t['Key']: t for t in asg.get('Tags', [])} 

1397 source = tag_map[source_tag] 

1398 destination_tag = self.data.get('dest') 

1399 propagate = self.data.get('propagate', True) 

1400 client = local_session( 

1401 self.manager.session_factory).client('autoscaling') 

1402 # technically safer to create first, but running into 

1403 # max tags constraints, otherwise. 

1404 # 

1405 # delete_first = len([t for t in tag_map if not t.startswith('aws:')]) 

1406 client.delete_tags(Tags=[ 

1407 {'ResourceId': asg['AutoScalingGroupName'], 

1408 'ResourceType': 'auto-scaling-group', 

1409 'Key': source_tag, 

1410 'Value': source['Value']}]) 

1411 client.create_or_update_tags(Tags=[ 

1412 {'ResourceId': asg['AutoScalingGroupName'], 

1413 'ResourceType': 'auto-scaling-group', 

1414 'PropagateAtLaunch': propagate, 

1415 'Key': destination_tag, 

1416 'Value': source['Value']}]) 

1417 if propagate and asg['Instances']: 

1418 self.propagate_instance_tag(source, destination_tag, asg) 

1419 

1420 def propagate_instance_tag(self, source, destination_tag, asg): 

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

1422 client.delete_tags( 

1423 Resources=[i['InstanceId'] for i in asg['Instances']], 

1424 Tags=[{"Key": source['Key']}]) 

1425 client.create_tags( 

1426 Resources=[i['InstanceId'] for i in asg['Instances']], 

1427 Tags=[{'Key': destination_tag, 'Value': source['Value']}]) 

1428 

1429 

1430@ASG.action_registry.register('mark-for-op') 

1431class MarkForOp(TagDelayedAction): 

1432 """Action to create a delayed action for a later date 

1433 

1434 :example: 

1435 

1436 .. code-block:: yaml 

1437 

1438 policies: 

1439 - name: asg-suspend-schedule 

1440 resource: asg 

1441 filters: 

1442 - type: value 

1443 key: MinSize 

1444 value: 2 

1445 actions: 

1446 - type: mark-for-op 

1447 tag: custodian_suspend 

1448 message: "Suspending: {op}@{action_date}" 

1449 op: suspend 

1450 days: 7 

1451 """ 

1452 

1453 schema = type_schema( 

1454 'mark-for-op', 

1455 op={'type': 'string'}, 

1456 key={'type': 'string'}, 

1457 tag={'type': 'string'}, 

1458 tz={'type': 'string'}, 

1459 msg={'type': 'string'}, 

1460 message={'type': 'string'}, 

1461 days={'type': 'number', 'minimum': 0}, 

1462 hours={'type': 'number', 'minimum': 0}) 

1463 schema_alias = False 

1464 default_template = ( 

1465 'AutoScaleGroup does not meet org policy: {op}@{action_date}') 

1466 

1467 def get_config_values(self): 

1468 d = { 

1469 'op': self.data.get('op', 'stop'), 

1470 'tag': self.data.get('key', self.data.get('tag', DEFAULT_TAG)), 

1471 'msg': self.data.get('message', self.data.get('msg', self.default_template)), 

1472 'tz': self.data.get('tz', 'utc'), 

1473 'days': self.data.get('days', 0), 

1474 'hours': self.data.get('hours', 0)} 

1475 d['action_date'] = self.generate_timestamp( 

1476 d['days'], d['hours']) 

1477 return d 

1478 

1479 

1480@ASG.action_registry.register('suspend') 

1481class Suspend(Action): 

1482 """Action to suspend ASG processes and instances 

1483 

1484 AWS ASG suspend/resume and process docs 

1485 https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-suspend-resume-processes.html 

1486 

1487 :example: 

1488 

1489 .. code-block:: yaml 

1490 

1491 policies: 

1492 - name: asg-suspend-processes 

1493 resource: asg 

1494 filters: 

1495 - "tag:SuspendTag": present 

1496 actions: 

1497 - type: suspend 

1498 """ 

1499 permissions = ("autoscaling:SuspendProcesses", "ec2:StopInstances") 

1500 

1501 ASG_PROCESSES = [ 

1502 "Launch", 

1503 "Terminate", 

1504 "HealthCheck", 

1505 "ReplaceUnhealthy", 

1506 "AZRebalance", 

1507 "AlarmNotification", 

1508 "ScheduledActions", 

1509 "AddToLoadBalancer", 

1510 "InstanceRefresh"] 

1511 

1512 schema = type_schema( 

1513 'suspend', 

1514 exclude={ 

1515 'type': 'array', 

1516 'title': 'ASG Processes to not suspend', 

1517 'items': {'enum': ASG_PROCESSES}}) 

1518 

1519 ASG_PROCESSES = set(ASG_PROCESSES) 

1520 

1521 def process(self, asgs): 

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

1523 list(w.map(self.process_asg, asgs)) 

1524 

1525 def process_asg(self, asg): 

1526 """Multistep process to stop an asg aprori of setup 

1527 

1528 - suspend processes 

1529 - stop instances 

1530 """ 

1531 session = local_session(self.manager.session_factory) 

1532 asg_client = session.client('autoscaling') 

1533 processes = list(self.ASG_PROCESSES.difference( 

1534 self.data.get('exclude', ()))) 

1535 

1536 try: 

1537 self.manager.retry( 

1538 asg_client.suspend_processes, 

1539 ScalingProcesses=processes, 

1540 AutoScalingGroupName=asg['AutoScalingGroupName']) 

1541 except ClientError as e: 

1542 if e.response['Error']['Code'] == 'ValidationError': 

1543 return 

1544 raise 

1545 ec2_client = session.client('ec2') 

1546 try: 

1547 instance_ids = [i['InstanceId'] for i in asg['Instances']] 

1548 if not instance_ids: 

1549 return 

1550 retry = get_retry(( 

1551 'RequestLimitExceeded', 'Client.RequestLimitExceeded')) 

1552 retry(ec2_client.stop_instances, InstanceIds=instance_ids) 

1553 except ClientError as e: 

1554 if e.response['Error']['Code'] in ( 

1555 'UnsupportedOperation', 

1556 'InvalidInstanceID.NotFound', 

1557 'IncorrectInstanceState'): 

1558 self.log.warning("Erroring stopping asg instances %s %s" % ( 

1559 asg['AutoScalingGroupName'], e)) 

1560 return 

1561 raise 

1562 

1563 

1564@ASG.action_registry.register('resume') 

1565class Resume(Action): 

1566 """Resume a suspended autoscale group and its instances 

1567 

1568 Parameter 'delay' is the amount of time (in seconds) to wait 

1569 between resuming instances in the asg, and restarting the internal 

1570 asg processed which gives some grace period before health checks 

1571 turn on within the ASG (default value: 30) 

1572 

1573 :example: 

1574 

1575 .. code-block:: yaml 

1576 

1577 policies: 

1578 - name: asg-resume-processes 

1579 resource: asg 

1580 filters: 

1581 - "tag:Resume": present 

1582 actions: 

1583 - type: resume 

1584 delay: 300 

1585 

1586 """ 

1587 ASG_PROCESSES = Suspend.ASG_PROCESSES 

1588 schema = type_schema( 

1589 'resume', 

1590 exclude={ 

1591 'type': 'array', 

1592 'title': 'ASG Processes to not resume', 

1593 'items': {'enum': list(ASG_PROCESSES)}}, 

1594 delay={'type': 'number'}) 

1595 

1596 permissions = ("autoscaling:ResumeProcesses", "ec2:StartInstances") 

1597 

1598 def process(self, asgs): 

1599 original_count = len(asgs) 

1600 asgs = [a for a in asgs if a['SuspendedProcesses']] 

1601 self.delay = self.data.get('delay', 30) 

1602 self.log.debug("Filtered from %d to %d suspended asgs", 

1603 original_count, len(asgs)) 

1604 

1605 session = local_session(self.manager.session_factory) 

1606 ec2_client = session.client('ec2') 

1607 asg_client = session.client('autoscaling') 

1608 

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

1610 futures = {} 

1611 for a in asgs: 

1612 futures[w.submit(self.resume_asg_instances, ec2_client, a)] = a 

1613 for f in as_completed(futures): 

1614 if f.exception(): 

1615 self.log.error("Traceback resume asg:%s instances error:%s" % ( 

1616 futures[f]['AutoScalingGroupName'], 

1617 f.exception())) 

1618 continue 

1619 

1620 self.log.debug("Sleeping for asg health check grace") 

1621 time.sleep(self.delay) 

1622 

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

1624 futures = {} 

1625 for a in asgs: 

1626 futures[w.submit(self.resume_asg, asg_client, a)] = a 

1627 for f in as_completed(futures): 

1628 if f.exception(): 

1629 self.log.error("Traceback resume asg:%s error:%s" % ( 

1630 futures[f]['AutoScalingGroupName'], 

1631 f.exception())) 

1632 

1633 def resume_asg_instances(self, ec2_client, asg): 

1634 """Resume asg instances. 

1635 """ 

1636 instance_ids = [i['InstanceId'] for i in asg['Instances']] 

1637 if not instance_ids: 

1638 return 

1639 retry = get_retry(( 

1640 'RequestLimitExceeded', 'Client.RequestLimitExceeded')) 

1641 retry(ec2_client.start_instances, InstanceIds=instance_ids) 

1642 

1643 def resume_asg(self, asg_client, asg): 

1644 """Resume asg processes. 

1645 """ 

1646 processes = list(self.ASG_PROCESSES.difference( 

1647 self.data.get('exclude', ()))) 

1648 

1649 self.manager.retry( 

1650 asg_client.resume_processes, 

1651 ScalingProcesses=processes, 

1652 AutoScalingGroupName=asg['AutoScalingGroupName']) 

1653 

1654 

1655@ASG.action_registry.register('delete') 

1656class Delete(Action): 

1657 """Action to delete an ASG 

1658 

1659 The 'force' parameter is needed when deleting an ASG that has instances 

1660 attached to it. 

1661 

1662 :example: 

1663 

1664 .. code-block:: yaml 

1665 

1666 policies: 

1667 - name: asg-delete-bad-encryption 

1668 resource: asg 

1669 filters: 

1670 - type: not-encrypted 

1671 exclude_image: true 

1672 actions: 

1673 - type: delete 

1674 force: true 

1675 """ 

1676 

1677 schema = type_schema('delete', force={'type': 'boolean'}) 

1678 permissions = ("autoscaling:DeleteAutoScalingGroup",) 

1679 

1680 def process(self, asgs): 

1681 client = local_session( 

1682 self.manager.session_factory).client('autoscaling') 

1683 for asg in asgs: 

1684 self.process_asg(client, asg) 

1685 

1686 def process_asg(self, client, asg): 

1687 force_delete = self.data.get('force', False) 

1688 try: 

1689 self.manager.retry( 

1690 client.delete_auto_scaling_group, 

1691 AutoScalingGroupName=asg['AutoScalingGroupName'], 

1692 ForceDelete=force_delete) 

1693 except ClientError as e: 

1694 if e.response['Error']['Code'] == 'ValidationError': 

1695 return 

1696 raise 

1697 

1698 

1699@ASG.action_registry.register('update') 

1700class Update(Action): 

1701 """Action to update ASG configuration settings 

1702 

1703 :example: 

1704 

1705 .. code-block:: yaml 

1706 

1707 policies: 

1708 - name: set-asg-instance-lifetime 

1709 resource: asg 

1710 filters: 

1711 - MaxInstanceLifetime: empty 

1712 actions: 

1713 - type: update 

1714 max-instance-lifetime: 604800 # (7 days) 

1715 

1716 - name: set-asg-by-policy 

1717 resource: asg 

1718 actions: 

1719 - type: update 

1720 default-cooldown: 600 

1721 max-instance-lifetime: 0 # (clear it) 

1722 new-instances-protected-from-scale-in: true 

1723 capacity-rebalance: true 

1724 """ 

1725 

1726 schema = type_schema( 

1727 'update', 

1728 **{ 

1729 'default-cooldown': {'type': 'integer', 'minimum': 0}, 

1730 'max-instance-lifetime': { 

1731 "anyOf": [ 

1732 {'enum': [0]}, 

1733 {'type': 'integer', 'minimum': 86400} 

1734 ] 

1735 }, 

1736 'new-instances-protected-from-scale-in': {'type': 'boolean'}, 

1737 'capacity-rebalance': {'type': 'boolean'}, 

1738 } 

1739 ) 

1740 permissions = ("autoscaling:UpdateAutoScalingGroup",) 

1741 settings_map = { 

1742 "default-cooldown": "DefaultCooldown", 

1743 "max-instance-lifetime": "MaxInstanceLifetime", 

1744 "new-instances-protected-from-scale-in": "NewInstancesProtectedFromScaleIn", 

1745 "capacity-rebalance": "CapacityRebalance" 

1746 } 

1747 

1748 def validate(self): 

1749 if not set(self.settings_map).intersection(set(self.data)): 

1750 raise PolicyValidationError( 

1751 "At least one setting must be specified from: " + 

1752 ", ".join(sorted(self.settings_map)) 

1753 ) 

1754 return self 

1755 

1756 def process(self, asgs): 

1757 client = local_session(self.manager.session_factory).client('autoscaling') 

1758 

1759 settings = {} 

1760 for k, v in self.settings_map.items(): 

1761 if k in self.data: 

1762 settings[v] = self.data.get(k) 

1763 

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

1765 futures = {} 

1766 error = None 

1767 for a in asgs: 

1768 futures[w.submit(self.process_asg, client, a, settings)] = a 

1769 for f in as_completed(futures): 

1770 if f.exception(): 

1771 self.log.error("Error while updating asg:%s error:%s" % ( 

1772 futures[f]['AutoScalingGroupName'], 

1773 f.exception())) 

1774 error = f.exception() 

1775 if error: 

1776 # make sure we stop policy execution if there were errors 

1777 raise error 

1778 

1779 def process_asg(self, client, asg, settings): 

1780 self.manager.retry( 

1781 client.update_auto_scaling_group, 

1782 AutoScalingGroupName=asg['AutoScalingGroupName'], 

1783 **settings) 

1784 

1785 

1786@resources.register('launch-config') 

1787class LaunchConfig(query.QueryResourceManager): 

1788 

1789 class resource_type(query.TypeInfo): 

1790 service = 'autoscaling' 

1791 arn_type = 'launchConfiguration' 

1792 id = name = 'LaunchConfigurationName' 

1793 date = 'CreatedTime' 

1794 enum_spec = ( 

1795 'describe_launch_configurations', 'LaunchConfigurations', None) 

1796 filter_name = 'LaunchConfigurationNames' 

1797 filter_type = 'list' 

1798 cfn_type = config_type = 'AWS::AutoScaling::LaunchConfiguration' 

1799 

1800 

1801@LaunchConfig.filter_registry.register('age') 

1802class LaunchConfigAge(AgeFilter): 

1803 """Filter ASG launch configuration by age (in days) 

1804 

1805 :example: 

1806 

1807 .. code-block:: yaml 

1808 

1809 policies: 

1810 - name: asg-launch-config-old 

1811 resource: launch-config 

1812 filters: 

1813 - type: age 

1814 days: 90 

1815 op: ge 

1816 """ 

1817 

1818 date_attribute = "CreatedTime" 

1819 schema = type_schema( 

1820 'age', 

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

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

1823 

1824 

1825@LaunchConfig.filter_registry.register('unused') 

1826class UnusedLaunchConfig(Filter): 

1827 """Filters all launch configurations that are not in use but exist 

1828 

1829 :example: 

1830 

1831 .. code-block:: yaml 

1832 

1833 policies: 

1834 - name: asg-unused-launch-config 

1835 resource: launch-config 

1836 filters: 

1837 - unused 

1838 """ 

1839 

1840 schema = type_schema('unused') 

1841 

1842 def get_permissions(self): 

1843 return self.manager.get_resource_manager('asg').get_permissions() 

1844 

1845 def process(self, configs, event=None): 

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

1847 used = {a.get('LaunchConfigurationName', a['AutoScalingGroupName']) 

1848 for a in asgs if not a.get('LaunchTemplate')} 

1849 return [c for c in configs if c['LaunchConfigurationName'] not in used] 

1850 

1851 

1852@LaunchConfig.action_registry.register('delete') 

1853class LaunchConfigDelete(Action): 

1854 """Filters all unused launch configurations 

1855 

1856 :example: 

1857 

1858 .. code-block:: yaml 

1859 

1860 policies: 

1861 - name: asg-unused-launch-config-delete 

1862 resource: launch-config 

1863 filters: 

1864 - unused 

1865 actions: 

1866 - delete 

1867 """ 

1868 

1869 schema = type_schema('delete') 

1870 permissions = ("autoscaling:DeleteLaunchConfiguration",) 

1871 

1872 def process(self, configs): 

1873 client = local_session(self.manager.session_factory).client('autoscaling') 

1874 

1875 for c in configs: 

1876 self.process_config(client, c) 

1877 

1878 def process_config(self, client, config): 

1879 try: 

1880 client.delete_launch_configuration( 

1881 LaunchConfigurationName=config[ 

1882 'LaunchConfigurationName']) 

1883 except ClientError as e: 

1884 # Catch already deleted 

1885 if e.response['Error']['Code'] == 'ValidationError': 

1886 return 

1887 raise 

1888 

1889 

1890@resources.register('scaling-policy') 

1891class ScalingPolicy(query.QueryResourceManager): 

1892 

1893 class resource_type(query.TypeInfo): 

1894 service = 'autoscaling' 

1895 arn_type = "scalingPolicy" 

1896 id = name = 'PolicyName' 

1897 date = 'CreatedTime' 

1898 enum_spec = ( 

1899 'describe_policies', 'ScalingPolicies', None 

1900 ) 

1901 filter_name = 'PolicyNames' 

1902 filter_type = 'list' 

1903 config_type = cfn_type = 'AWS::AutoScaling::ScalingPolicy' 

1904 

1905 

1906@ASG.filter_registry.register('scaling-policy') 

1907class ScalingPolicyFilter(ValueFilter): 

1908 

1909 """Filter asg by scaling-policies attributes. 

1910 

1911 :example: 

1912 

1913 .. code-block:: yaml 

1914 

1915 policies: 

1916 - name: scaling-policies-with-target-tracking 

1917 resource: asg 

1918 filters: 

1919 - type: scaling-policy 

1920 key: PolicyType 

1921 value: "TargetTrackingScaling" 

1922 

1923 """ 

1924 

1925 schema = type_schema( 

1926 'scaling-policy', rinherit=ValueFilter.schema 

1927 ) 

1928 schema_alias = False 

1929 permissions = ("autoscaling:DescribePolicies",) 

1930 annotate = False # no default value annotation on policy 

1931 annotation_key = 'c7n:matched-policies' 

1932 

1933 def get_scaling_policies(self, asgs): 

1934 policies = self.manager.get_resource_manager('scaling-policy').resources() 

1935 policy_map = {} 

1936 for policy in policies: 

1937 policy_map.setdefault( 

1938 policy['AutoScalingGroupName'], []).append(policy) 

1939 return policy_map 

1940 

1941 def process(self, asgs, event=None): 

1942 self.policy_map = self.get_scaling_policies(asgs) 

1943 return super(ScalingPolicyFilter, self).process(asgs, event) 

1944 

1945 def __call__(self, asg): 

1946 asg_policies = self.policy_map.get(asg['AutoScalingGroupName'], ()) 

1947 matched = [] 

1948 for policy in asg_policies: 

1949 if self.match(policy): 

1950 matched.append(policy) 

1951 if matched: 

1952 asg[self.annotation_key] = matched 

1953 return bool(matched)