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

839 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 

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 date = 'CreatedTime' 

40 dimension = 'AutoScalingGroupName' 

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

42 filter_name = 'AutoScalingGroupNames' 

43 filter_type = 'list' 

44 config_type = 'AWS::AutoScaling::AutoScalingGroup' 

45 cfn_type = 'AWS::AutoScaling::AutoScalingGroup' 

46 

47 default_report_fields = ( 

48 'AutoScalingGroupName', 

49 'CreatedTime', 

50 'LaunchConfigurationName', 

51 'count:Instances', 

52 'DesiredCapacity', 

53 'HealthCheckType', 

54 'list:LoadBalancerNames', 

55 ) 

56 

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

58 

59 

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

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

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

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

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

65 

66 

67class LaunchInfo: 

68 

69 permissions = ("ec2:DescribeLaunchTemplateVersions", 

70 "autoscaling:DescribeLaunchConfigurations",) 

71 

72 def __init__(self, manager): 

73 self.manager = manager 

74 

75 def initialize(self, asgs): 

76 self.templates = self.get_launch_templates(asgs) 

77 self.configs = self.get_launch_configs(asgs) 

78 return self 

79 

80 def get_launch_templates(self, asgs): 

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

82 # template ids include version identifiers 

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

84 if not template_ids: 

85 return {} 

86 return { 

87 (t['LaunchTemplateId'], 

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

89 for t in tmpl_mgr.get_resources(template_ids)} 

90 

91 def get_launch_configs(self, asgs): 

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

93 config_names = set() 

94 for a in asgs: 

95 if 'LaunchConfigurationName' not in a: 

96 continue 

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

98 if not config_names: 

99 return {} 

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

101 if len(config_names) < 5: 

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

103 else: 

104 configs = lc_resources.resources() 

105 return { 

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

107 if cfg['LaunchConfigurationName'] in config_names} 

108 

109 def get_launch_id(self, asg): 

110 lid = asg.get('LaunchConfigurationName') 

111 if lid is not None: 

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

113 return lid.strip() 

114 

115 lid = asg.get('LaunchTemplate') 

116 if lid is not None: 

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

118 

119 if 'MixedInstancesPolicy' in asg: 

120 mip_spec = asg['MixedInstancesPolicy'][ 

121 'LaunchTemplate']['LaunchTemplateSpecification'] 

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

123 

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

125 # explicitly specified as launchconfiguration attribute. 

126 lid = asg['AutoScalingGroupName'] 

127 return lid 

128 

129 def get(self, asg): 

130 lid = self.get_launch_id(asg) 

131 if isinstance(lid, tuple): 

132 return self.templates.get(lid) 

133 else: 

134 return self.configs.get(lid) 

135 

136 def items(self): 

137 return itertools.chain(*( 

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

139 

140 def get_image_ids(self): 

141 image_ids = {} 

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

143 if c.get('ImageId'): 

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

145 return image_ids 

146 

147 def get_image_map(self): 

148 # The describe_images api historically would return errors 

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

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

151 # ideally for our use case. 

152 # 

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

154 # the resource manager abstraction to take advantage of 

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

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

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

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

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

160 # 

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

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

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

164 # not in the account. 

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

166 self.manager.get_resource_manager( 

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

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

169 

170 def get_security_group_ids(self): 

171 # return set of security group ids for given asg 

172 sg_ids = set() 

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

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

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

176 return sg_ids 

177 

178 

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

180class SecurityGroupFilter(net_filters.SecurityGroupFilter): 

181 

182 RelatedIdsExpression = "" 

183 

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

185 

186 def get_related_ids(self, asgs): 

187 return self.launch_info.get_security_group_ids() 

188 

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

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

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

192 

193 

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

195class SubnetFilter(net_filters.SubnetFilter): 

196 

197 RelatedIdsExpression = "" 

198 

199 def get_related_ids(self, asgs): 

200 subnet_ids = set() 

201 for asg in asgs: 

202 subnet_ids.update( 

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

204 return subnet_ids 

205 

206 

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

208class LaunchConfigFilter(ValueFilter): 

209 """Filter asg by launch config attributes. 

210 

211 This will also filter to launch template data in addition 

212 to launch configurations. 

213 

214 :example: 

215 

216 .. code-block:: yaml 

217 

218 policies: 

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

220 resource: asg 

221 filters: 

222 - type: launch-config 

223 key: AssociatePublicIpAddress 

224 value: true 

225 """ 

226 schema = type_schema( 

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

228 schema_alias = False 

229 permissions = ("autoscaling:DescribeLaunchConfigurations",) 

230 

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

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

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

234 

235 def __call__(self, asg): 

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

237 

238 

239class ConfigValidFilter(Filter): 

240 

241 def get_permissions(self): 

242 return list(itertools.chain(*[ 

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

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

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

246 

247 def validate(self): 

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

249 raise PolicyValidationError( 

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

251 return self 

252 

253 def initialize(self, asgs): 

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

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

256 self.subnets = self.get_subnets() 

257 self.security_groups = self.get_security_groups() 

258 self.key_pairs = self.get_key_pairs() 

259 self.elbs = self.get_elbs() 

260 self.appelb_target_groups = self.get_appelb_target_groups() 

261 self.snapshots = self.get_snapshots() 

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

263 

264 def get_subnets(self): 

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

266 return {s['SubnetId'] for s in manager.resources()} 

267 

268 def get_security_groups(self): 

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

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

271 

272 def get_key_pairs(self): 

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

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

275 

276 def get_elbs(self): 

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

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

279 

280 def get_appelb_target_groups(self): 

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

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

283 

284 def get_images(self): 

285 images = self.launch_info.get_image_map() 

286 image_snaps = set() 

287 

288 for a in images.values(): 

289 # Capture any snapshots, images strongly reference their 

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

291 # case of a third party image. 

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

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

294 continue 

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

296 return set(images), image_snaps 

297 

298 def get_snapshots(self): 

299 snaps = set() 

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

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

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

303 continue 

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

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

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

307 list(snaps), cache=False)} 

308 

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

310 self.initialize(asgs) 

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

312 

313 def get_asg_errors(self, asg): 

314 errors = [] 

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

316 

317 for subnet in subnets: 

318 subnet = subnet.strip() 

319 if subnet not in self.subnets: 

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

321 

322 for elb in asg['LoadBalancerNames']: 

323 elb = elb.strip() 

324 if elb not in self.elbs: 

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

326 

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

328 appelb_target = appelb_target.strip() 

329 if appelb_target not in self.appelb_target_groups: 

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

331 

332 cfg_id = self.launch_info.get_launch_id(asg) 

333 cfg = self.launch_info.get(asg) 

334 

335 if cfg is None: 

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

337 self.log.debug( 

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

339 asg['Invalid'] = errors 

340 return True 

341 

342 for sg in itertools.chain(*( 

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

344 sg = sg.strip() 

345 if sg not in self.security_groups: 

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

347 

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

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

350 

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

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

353 

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

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

356 continue 

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

358 if snapshot_id in self.image_snaps: 

359 continue 

360 if snapshot_id not in self.snapshots: 

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

362 return errors 

363 

364 

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

366class ValidConfigFilter(ConfigValidFilter): 

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

368 

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

370 workflows. 

371 

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

373 

374 :example: 

375 

376 .. code-block:: yaml 

377 

378 policies: 

379 - name: asg-valid-config 

380 resource: asg 

381 filters: 

382 - valid 

383 """ 

384 

385 schema = type_schema('valid') 

386 

387 def __call__(self, asg): 

388 errors = self.get_asg_errors(asg) 

389 return not bool(errors) 

390 

391 

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

393class InvalidConfigFilter(ConfigValidFilter): 

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

395 

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

397 to launch an instance succesfully as the configuration has 

398 

399 - invalid subnets 

400 - invalid security groups 

401 - invalid key pair name 

402 - invalid launch config volume snapshots 

403 - invalid amis 

404 - invalid health check elb (slower) 

405 

406 Internally this tries to reuse other resource managers for better 

407 cache utilization. 

408 

409 :example: 

410 

411 .. code-block:: yaml 

412 

413 policies: 

414 - name: asg-invalid-config 

415 resource: asg 

416 filters: 

417 - invalid 

418 """ 

419 schema = type_schema('invalid') 

420 

421 def __call__(self, asg): 

422 errors = self.get_asg_errors(asg) 

423 if errors: 

424 asg['Invalid'] = errors 

425 return True 

426 

427 

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

429class NotEncryptedFilter(Filter): 

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

431 

432 Checks both the ami snapshots and the launch configuration. 

433 

434 :example: 

435 

436 .. code-block:: yaml 

437 

438 policies: 

439 - name: asg-unencrypted 

440 resource: asg 

441 filters: 

442 - type: not-encrypted 

443 exclude_image: true 

444 """ 

445 

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

447 permissions = ( 

448 'ec2:DescribeImages', 

449 'ec2:DescribeSnapshots', 

450 'autoscaling:DescribeLaunchConfigurations') 

451 

452 images = unencrypted_configs = unencrypted_images = None 

453 

454 # TODO: resource-manager, notfound err mgr 

455 

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

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

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

459 

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

461 self.unencrypted_images = self.get_unencrypted_images() 

462 

463 self.unencrypted_launch = self.get_unencrypted_configs() 

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

465 

466 def __call__(self, asg): 

467 launch = self.launch_info.get(asg) 

468 if not launch: 

469 self.log.warning( 

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

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

472 return False 

473 

474 launch_id = self.launch_info.get_launch_id(asg) 

475 unencrypted = [] 

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

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

478 unencrypted.append('Image') 

479 

480 if launch_id in self.unencrypted_launch: 

481 unencrypted.append('LaunchConfig') 

482 if unencrypted: 

483 asg['Unencrypted'] = unencrypted 

484 return bool(unencrypted) 

485 

486 def get_unencrypted_images(self): 

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

488 unencrypted_images = set() 

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

490 for bd in i['BlockDeviceMappings']: 

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

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

493 break 

494 return unencrypted_images 

495 

496 def get_unencrypted_configs(self): 

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

498 unencrypted_configs = set() 

499 snaps = {} 

500 

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

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

503 # image deregistered/unavailable or exclude_image set 

504 if image is not None: 

505 image_block_devs = { 

506 bd['DeviceName'] for bd in 

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

508 else: 

509 image_block_devs = set() 

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

511 if 'Ebs' not in bd: 

512 continue 

513 # Launch configs can shadow image devices, images have 

514 # precedence. 

515 if bd['DeviceName'] in image_block_devs: 

516 continue 

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

518 snaps.setdefault( 

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

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

521 unencrypted_configs.add(cid) 

522 if not snaps: 

523 return unencrypted_configs 

524 

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

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

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

528 return unencrypted_configs 

529 

530 def get_snapshots(self, snap_ids): 

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

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

533 snap_ids, cache=False) 

534 

535 

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

537class ImageAgeFilter(AgeFilter): 

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

539 

540 :example: 

541 

542 .. code-block:: yaml 

543 

544 policies: 

545 - name: asg-older-image 

546 resource: asg 

547 filters: 

548 - type: image-age 

549 days: 90 

550 op: ge 

551 """ 

552 permissions = ( 

553 "ec2:DescribeImages", 

554 "autoscaling:DescribeLaunchConfigurations") 

555 

556 date_attribute = "CreationDate" 

557 schema = type_schema( 

558 'image-age', 

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

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

561 

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

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

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

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

566 

567 def get_resource_date(self, asg): 

568 cfg = self.launch_info.get(asg) 

569 if cfg is None: 

570 cfg = {} 

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

572 return parse(ami.get( 

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

574 

575 

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

577class ImageFilter(ValueFilter): 

578 """Filter asg by image 

579 

580 :example: 

581 

582 .. code-block:: yaml 

583 

584 policies: 

585 - name: non-windows-asg 

586 resource: asg 

587 filters: 

588 - type: image 

589 key: Platform 

590 value: Windows 

591 op: ne 

592 """ 

593 permissions = ( 

594 "ec2:DescribeImages", 

595 "autoscaling:DescribeLaunchConfigurations") 

596 

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

598 schema_alias = True 

599 

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

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

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

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

604 

605 def __call__(self, i): 

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

607 image = self.images.get(image_id) 

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

609 if not image: 

610 self.log.warning( 

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

612 i['AutoScalingGroupName'], image_id )) 

613 # Match instead on empty skeleton? 

614 return False 

615 return self.match(image) 

616 

617 

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

619class VpcIdFilter(ValueFilter): 

620 """Filters ASG based on the VpcId 

621 

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

623 associated to the results from describing the autoscaling groups. 

624 

625 :example: 

626 

627 .. code-block:: yaml 

628 

629 policies: 

630 - name: asg-vpc-xyz 

631 resource: asg 

632 filters: 

633 - type: vpc-id 

634 value: vpc-12ab34cd 

635 """ 

636 

637 schema = type_schema( 

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

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

640 schema_alias = False 

641 permissions = ('ec2:DescribeSubnets',) 

642 

643 # TODO: annotation 

644 

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

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

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

648 

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

650 subnets = {} 

651 for a in asgs: 

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

653 if not subnet_ids: 

654 continue 

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

656 

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

658 # Invalid subnets on asgs happen, so query all 

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

660 

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

662 if s not in all_subnets: 

663 self.log.warning( 

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

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

666 continue 

667 for a in s_asgs: 

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

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

670 

671 

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

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

674class PropagatedTagFilter(Filter): 

675 """Filter ASG based on propagated tags 

676 

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

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

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

680 account for compliance. 

681 

682 :example: 

683 

684 .. code-block:: yaml 

685 

686 policies: 

687 - name: asg-non-propagated-tags 

688 resource: asg 

689 filters: 

690 - type: propagated-tags 

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

692 match: false 

693 propagate: true 

694 """ 

695 schema = type_schema( 

696 'progagated-tags', 

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

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

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

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

701 permissions = ( 

702 "autoscaling:DescribeLaunchConfigurations", 

703 "autoscaling:DescribeAutoScalingGroups") 

704 

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

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

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

708 results = [] 

709 for asg in asgs: 

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

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

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

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

714 results.append(asg) 

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

716 results.append(asg) 

717 else: 

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

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

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

721 results.append(asg) 

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

723 results.append(asg) 

724 return results 

725 

726 

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

728class AsgPostFinding(PostFinding): 

729 

730 resource_type = 'AwsAutoScalingAutoScalingGroup' 

731 launch_info = LaunchInfo(None) 

732 

733 def format_resource(self, r): 

734 envelope, payload = self.format_envelope(r) 

735 details = select_keys(r, [ 

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

737 lid = self.launch_info.get_launch_id(r) 

738 if isinstance(lid, tuple): 

739 lid = "%s:%s" % lid 

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

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

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

743 payload.update(details) 

744 return envelope 

745 

746 

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

748class AutoScaleAutoTagUser(AutoTagUser): 

749 

750 schema = type_schema( 

751 'auto-tag-user', 

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

753 rinherit=AutoTagUser.schema) 

754 schema_alias = False 

755 

756 def set_resource_tags(self, tags, resources): 

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

758 tag_action( 

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

760 self.manager).process(resources) 

761 

762 

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

764class GroupTagTrim(TagTrim): 

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

766 

767 :example: 

768 

769 .. code-block:: yaml 

770 

771 policies: 

772 - name: asg-tag-trim 

773 resource: asg 

774 filters: 

775 - type: tag-count 

776 count: 10 

777 actions: 

778 - type: tag-trim 

779 space: 1 

780 preserve: 

781 - OwnerName 

782 - OwnerContact 

783 """ 

784 

785 max_tag_count = 10 

786 permissions = ('autoscaling:DeleteTags',) 

787 

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

789 tags = [] 

790 for t in candidates: 

791 tags.append( 

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

793 ResourceId=resource['AutoScalingGroupName'])) 

794 client.delete_tags(Tags=tags) 

795 

796 

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

798class CapacityDelta(Filter): 

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

800 

801 :example: 

802 

803 .. code-block:: yaml 

804 

805 policies: 

806 - name: asg-capacity-delta 

807 resource: asg 

808 filters: 

809 - capacity-delta 

810 """ 

811 

812 schema = type_schema('capacity-delta') 

813 

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

815 return [ 

816 a for a in asgs if len( 

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

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

819 

820 

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

822class UserDataFilter(ValueFilter): 

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

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

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

826 

827 :example: 

828 

829 .. code-block:: yaml 

830 

831 policies: 

832 - name: lc_userdata 

833 resource: asg 

834 filters: 

835 - type: user-data 

836 op: regex 

837 value: (?smi).*password= 

838 actions: 

839 - delete 

840 """ 

841 

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

843 schema_alias = False 

844 batch_size = 50 

845 annotation = 'c7n:user-data' 

846 

847 def __init__(self, data, manager): 

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

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

850 

851 def get_permissions(self): 

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

853 

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

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

856 user-data filter. 

857 

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

859 ''' 

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

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

862 

863 results = [] 

864 for asg in asgs: 

865 launch_config = launch_info.get(asg) 

866 if self.annotation not in launch_config: 

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

868 asg[self.annotation] = None 

869 else: 

870 asg[self.annotation] = deserialize_user_data( 

871 launch_config['UserData']) 

872 if self.match(asg): 

873 results.append(asg) 

874 return results 

875 

876 

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

878class Resize(Action): 

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

880 

881 There are several ways to use this action: 

882 

883 1. set min/desired to current running instances 

884 

885 .. code-block:: yaml 

886 

887 policies: 

888 - name: asg-resize 

889 resource: asg 

890 filters: 

891 - capacity-delta 

892 actions: 

893 - type: resize 

894 desired-size: "current" 

895 

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

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

898 

899 .. code-block:: yaml 

900 

901 policies: 

902 - name: offhours-asg-off 

903 resource: asg 

904 filters: 

905 - type: offhour 

906 offhour: 19 

907 default_tz: bst 

908 actions: 

909 - type: resize 

910 min-size: 0 

911 desired-size: 0 

912 save-options-tag: OffHoursPrevious 

913 

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

915 

916 .. code-block:: yaml 

917 

918 policies: 

919 - name: offhours-asg-on 

920 resource: asg 

921 filters: 

922 - type: onhour 

923 onhour: 8 

924 default_tz: bst 

925 actions: 

926 - type: resize 

927 restore-options-tag: OffHoursPrevious 

928 

929 """ 

930 

931 schema = type_schema( 

932 'resize', 

933 **{ 

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

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

936 'desired-size': { 

937 "anyOf": [ 

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

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

940 ] 

941 }, 

942 # support previous key name with underscore 

943 'desired_size': { 

944 "anyOf": [ 

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

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

947 ] 

948 }, 

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

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

951 } 

952 ) 

953 permissions = ( 

954 'autoscaling:UpdateAutoScalingGroup', 

955 'autoscaling:CreateOrUpdateTags' 

956 ) 

957 

958 def process(self, asgs): 

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

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

961 

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

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

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

965 

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

967 'autoscaling') 

968 for a in asgs: 

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

970 update = {} 

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

972 

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

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

975 self.log.debug( 

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

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

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

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

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

981 if param in asg_params: 

982 update[param] = int(value) 

983 

984 else: 

985 # we want to resize, parse provided params 

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

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

988 

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

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

991 

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

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

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

995 if 'MinSize' not in update: 

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

997 # ensure it is at least as low as current_size 

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

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

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

1001 

1002 if update: 

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

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

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

1006 

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

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

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

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

1011 tags = [dict( 

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

1013 PropagateAtLaunch=False, 

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

1015 ResourceId=a['AutoScalingGroupName'], 

1016 ResourceType='auto-scaling-group', 

1017 )] 

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

1019 

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

1021 str(update))) 

1022 self.manager.retry( 

1023 client.update_auto_scaling_group, 

1024 AutoScalingGroupName=a['AutoScalingGroupName'], 

1025 **update) 

1026 else: 

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

1028 

1029 

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

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

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

1033class RemoveTag(Action): 

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

1035 

1036 :example: 

1037 

1038 .. code-block:: yaml 

1039 

1040 policies: 

1041 - name: asg-remove-unnecessary-tags 

1042 resource: asg 

1043 filters: 

1044 - "tag:UnnecessaryTag": present 

1045 actions: 

1046 - type: remove-tag 

1047 key: UnnecessaryTag 

1048 """ 

1049 

1050 schema = type_schema( 

1051 'remove-tag', 

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

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

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

1055 

1056 permissions = ('autoscaling:DeleteTags',) 

1057 batch_size = 1 

1058 

1059 def process(self, asgs): 

1060 error = False 

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

1062 if not tags: 

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

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

1065 

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

1067 futures = {} 

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

1069 futures[w.submit( 

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

1071 for f in as_completed(futures): 

1072 asg_set = futures[f] 

1073 if f.exception(): 

1074 error = f.exception() 

1075 self.log.exception( 

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

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

1078 for a in asg_set]), 

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

1080 f.exception())) 

1081 if error: 

1082 raise error 

1083 

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

1085 tag_set = [] 

1086 for a in asgs: 

1087 for t in tags: 

1088 tag_set.append(dict( 

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

1090 ResourceId=a['AutoScalingGroupName'])) 

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

1092 

1093 

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

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

1096class Tag(Action): 

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

1098 

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

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

1101 to the ASG itself. 

1102 

1103 :example: 

1104 

1105 .. code-block:: yaml 

1106 

1107 policies: 

1108 - name: asg-add-owner-tag 

1109 resource: asg 

1110 filters: 

1111 - "tag:OwnerName": absent 

1112 actions: 

1113 - type: tag 

1114 key: OwnerName 

1115 value: OwnerName 

1116 propagate: true 

1117 """ 

1118 

1119 schema = type_schema( 

1120 'tag', 

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

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

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

1124 # Backwards compatibility 

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

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

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

1128 aliases=('mark',) 

1129 ) 

1130 permissions = ('autoscaling:CreateOrUpdateTags',) 

1131 batch_size = 1 

1132 

1133 def get_tag_set(self): 

1134 tags = [] 

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

1136 value = self.data.get( 

1137 'value', self.data.get( 

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

1139 if key and value: 

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

1141 

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

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

1144 

1145 return tags 

1146 

1147 def process(self, asgs): 

1148 tags = self.get_tag_set() 

1149 error = None 

1150 

1151 self.interpolate_values(tags) 

1152 

1153 client = self.get_client() 

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

1155 futures = {} 

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

1157 futures[w.submit( 

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

1159 for f in as_completed(futures): 

1160 asg_set = futures[f] 

1161 if f.exception(): 

1162 self.log.exception( 

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

1164 tags, 

1165 f.exception(), 

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

1167 for a in asg_set]))) 

1168 if error: 

1169 raise error 

1170 

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

1172 tag_params = [] 

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

1174 for t in tags: 

1175 if 'PropagateAtLaunch' not in t: 

1176 t['PropagateAtLaunch'] = propagate 

1177 for t in tags: 

1178 for a in asgs: 

1179 atags = dict(t) 

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

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

1182 tag_params.append(atags) 

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

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

1185 

1186 def interpolate_values(self, tags): 

1187 params = { 

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

1189 'now': FormatDate.utcnow(), 

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

1191 for t in tags: 

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

1193 

1194 def get_client(self): 

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

1196 

1197 

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

1199class PropagateTags(Action): 

1200 """Propagate tags to an asg instances. 

1201 

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

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

1204 is applied to new instances. 

1205 

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

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

1208 the asg anymore that are present on instances. 

1209 

1210 :example: 

1211 

1212 .. code-block:: yaml 

1213 

1214 policies: 

1215 - name: asg-propagate-required 

1216 resource: asg 

1217 filters: 

1218 - "tag:OwnerName": present 

1219 actions: 

1220 - type: propagate-tags 

1221 tags: 

1222 - OwnerName 

1223 

1224 """ 

1225 

1226 schema = type_schema( 

1227 'propagate-tags', 

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

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

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

1231 

1232 def validate(self): 

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

1234 raise ValueError("No tags specified") 

1235 return self 

1236 

1237 def process(self, asgs): 

1238 if not asgs: 

1239 return 

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

1241 self.instance_map = self.get_instance_map(asgs) 

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

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

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

1245 

1246 def process_asg(self, asg): 

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

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

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

1250 

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

1252 tag_map = { 

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

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

1255 

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

1257 self.log.error( 

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

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

1260 

1261 tag_set = set(tag_map) 

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

1263 

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

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

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

1267 

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

1269 client.create_tags( 

1270 Resources=instance_ids, 

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

1272 return len(instance_ids) 

1273 

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

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

1276 on the asg. 

1277 """ 

1278 instance_tags = Counter() 

1279 instance_count = len(instances) 

1280 

1281 remove_tags = [] 

1282 extra_tags = [] 

1283 

1284 for i in instances: 

1285 instance_tags.update([ 

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

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

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

1289 if not v >= instance_count: 

1290 extra_tags.append(k) 

1291 continue 

1292 if k not in tag_set: 

1293 remove_tags.append(k) 

1294 

1295 if remove_tags: 

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

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

1298 if extra_tags: 

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

1300 asg['AutoScalingGroupName'], instance_tags)) 

1301 # Remove orphan tags 

1302 remove_tags.extend(extra_tags) 

1303 

1304 if not self.manager.config.dryrun: 

1305 client.delete_tags( 

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

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

1308 

1309 def get_instance_map(self, asgs): 

1310 instance_ids = [ 

1311 i['InstanceId'] for i in 

1312 list(itertools.chain(*[ 

1313 g['Instances'] 

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

1315 if not instance_ids: 

1316 return {} 

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

1318 self.manager.get_resource_manager( 

1319 'ec2').get_resources(instance_ids)} 

1320 

1321 

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

1323class RenameTag(Action): 

1324 """Rename a tag on an AutoScaleGroup. 

1325 

1326 :example: 

1327 

1328 .. code-block:: yaml 

1329 

1330 policies: 

1331 - name: asg-rename-owner-tag 

1332 resource: asg 

1333 filters: 

1334 - "tag:OwnerNames": present 

1335 actions: 

1336 - type: rename-tag 

1337 propagate: true 

1338 source: OwnerNames 

1339 dest: OwnerName 

1340 """ 

1341 

1342 schema = type_schema( 

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

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

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

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

1347 

1348 def get_permissions(self): 

1349 permissions = ( 

1350 'autoscaling:CreateOrUpdateTags', 

1351 'autoscaling:DeleteTags') 

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

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

1354 return permissions 

1355 

1356 def process(self, asgs): 

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

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

1359 count = len(asgs) 

1360 

1361 filtered = [] 

1362 for a in asgs: 

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

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

1365 filtered.append(a) 

1366 break 

1367 asgs = filtered 

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

1369 self.log.info( 

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

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

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

1373 

1374 def process_asg(self, asg): 

1375 """Move source tag to destination tag. 

1376 

1377 Check tag count on asg 

1378 Create new tag tag 

1379 Delete old tag 

1380 Check tag count on instance 

1381 Create new tag 

1382 Delete old tag 

1383 """ 

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

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

1386 source = tag_map[source_tag] 

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

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

1389 client = local_session( 

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

1391 # technically safer to create first, but running into 

1392 # max tags constraints, otherwise. 

1393 # 

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

1395 client.delete_tags(Tags=[ 

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

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

1398 'Key': source_tag, 

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

1400 client.create_or_update_tags(Tags=[ 

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

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

1403 'PropagateAtLaunch': propagate, 

1404 'Key': destination_tag, 

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

1406 if propagate and asg['Instances']: 

1407 self.propagate_instance_tag(source, destination_tag, asg) 

1408 

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

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

1411 client.delete_tags( 

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

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

1414 client.create_tags( 

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

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

1417 

1418 

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

1420class MarkForOp(TagDelayedAction): 

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

1422 

1423 :example: 

1424 

1425 .. code-block:: yaml 

1426 

1427 policies: 

1428 - name: asg-suspend-schedule 

1429 resource: asg 

1430 filters: 

1431 - type: value 

1432 key: MinSize 

1433 value: 2 

1434 actions: 

1435 - type: mark-for-op 

1436 tag: custodian_suspend 

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

1438 op: suspend 

1439 days: 7 

1440 """ 

1441 

1442 schema = type_schema( 

1443 'mark-for-op', 

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

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

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

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

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

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

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

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

1452 schema_alias = False 

1453 default_template = ( 

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

1455 

1456 def get_config_values(self): 

1457 d = { 

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

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

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

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

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

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

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

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

1466 return d 

1467 

1468 

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

1470class Suspend(Action): 

1471 """Action to suspend ASG processes and instances 

1472 

1473 AWS ASG suspend/resume and process docs 

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

1475 

1476 :example: 

1477 

1478 .. code-block:: yaml 

1479 

1480 policies: 

1481 - name: asg-suspend-processes 

1482 resource: asg 

1483 filters: 

1484 - "tag:SuspendTag": present 

1485 actions: 

1486 - type: suspend 

1487 """ 

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

1489 

1490 ASG_PROCESSES = [ 

1491 "Launch", 

1492 "Terminate", 

1493 "HealthCheck", 

1494 "ReplaceUnhealthy", 

1495 "AZRebalance", 

1496 "AlarmNotification", 

1497 "ScheduledActions", 

1498 "AddToLoadBalancer", 

1499 "InstanceRefresh"] 

1500 

1501 schema = type_schema( 

1502 'suspend', 

1503 exclude={ 

1504 'type': 'array', 

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

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

1507 

1508 ASG_PROCESSES = set(ASG_PROCESSES) 

1509 

1510 def process(self, asgs): 

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

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

1513 

1514 def process_asg(self, asg): 

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

1516 

1517 - suspend processes 

1518 - stop instances 

1519 """ 

1520 session = local_session(self.manager.session_factory) 

1521 asg_client = session.client('autoscaling') 

1522 processes = list(self.ASG_PROCESSES.difference( 

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

1524 

1525 try: 

1526 self.manager.retry( 

1527 asg_client.suspend_processes, 

1528 ScalingProcesses=processes, 

1529 AutoScalingGroupName=asg['AutoScalingGroupName']) 

1530 except ClientError as e: 

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

1532 return 

1533 raise 

1534 ec2_client = session.client('ec2') 

1535 try: 

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

1537 if not instance_ids: 

1538 return 

1539 retry = get_retry(( 

1540 'RequestLimitExceeded', 'Client.RequestLimitExceeded')) 

1541 retry(ec2_client.stop_instances, InstanceIds=instance_ids) 

1542 except ClientError as e: 

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

1544 'UnsupportedOperation', 

1545 'InvalidInstanceID.NotFound', 

1546 'IncorrectInstanceState'): 

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

1548 asg['AutoScalingGroupName'], e)) 

1549 return 

1550 raise 

1551 

1552 

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

1554class Resume(Action): 

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

1556 

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

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

1559 asg processed which gives some grace period before health checks 

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

1561 

1562 :example: 

1563 

1564 .. code-block:: yaml 

1565 

1566 policies: 

1567 - name: asg-resume-processes 

1568 resource: asg 

1569 filters: 

1570 - "tag:Resume": present 

1571 actions: 

1572 - type: resume 

1573 delay: 300 

1574 

1575 """ 

1576 schema = type_schema('resume', delay={'type': 'number'}) 

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

1578 

1579 def process(self, asgs): 

1580 original_count = len(asgs) 

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

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

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

1584 original_count, len(asgs)) 

1585 

1586 session = local_session(self.manager.session_factory) 

1587 ec2_client = session.client('ec2') 

1588 asg_client = session.client('autoscaling') 

1589 

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

1591 futures = {} 

1592 for a in asgs: 

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

1594 for f in as_completed(futures): 

1595 if f.exception(): 

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

1597 futures[f]['AutoScalingGroupName'], 

1598 f.exception())) 

1599 continue 

1600 

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

1602 time.sleep(self.delay) 

1603 

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

1605 futures = {} 

1606 for a in asgs: 

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

1608 for f in as_completed(futures): 

1609 if f.exception(): 

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

1611 futures[f]['AutoScalingGroupName'], 

1612 f.exception())) 

1613 

1614 def resume_asg_instances(self, ec2_client, asg): 

1615 """Resume asg instances. 

1616 """ 

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

1618 if not instance_ids: 

1619 return 

1620 retry = get_retry(( 

1621 'RequestLimitExceeded', 'Client.RequestLimitExceeded')) 

1622 retry(ec2_client.start_instances, InstanceIds=instance_ids) 

1623 

1624 def resume_asg(self, asg_client, asg): 

1625 """Resume asg processes. 

1626 """ 

1627 self.manager.retry( 

1628 asg_client.resume_processes, 

1629 AutoScalingGroupName=asg['AutoScalingGroupName']) 

1630 

1631 

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

1633class Delete(Action): 

1634 """Action to delete an ASG 

1635 

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

1637 attached to it. 

1638 

1639 :example: 

1640 

1641 .. code-block:: yaml 

1642 

1643 policies: 

1644 - name: asg-delete-bad-encryption 

1645 resource: asg 

1646 filters: 

1647 - type: not-encrypted 

1648 exclude_image: true 

1649 actions: 

1650 - type: delete 

1651 force: true 

1652 """ 

1653 

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

1655 permissions = ("autoscaling:DeleteAutoScalingGroup",) 

1656 

1657 def process(self, asgs): 

1658 client = local_session( 

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

1660 for asg in asgs: 

1661 self.process_asg(client, asg) 

1662 

1663 def process_asg(self, client, asg): 

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

1665 try: 

1666 self.manager.retry( 

1667 client.delete_auto_scaling_group, 

1668 AutoScalingGroupName=asg['AutoScalingGroupName'], 

1669 ForceDelete=force_delete) 

1670 except ClientError as e: 

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

1672 return 

1673 raise 

1674 

1675 

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

1677class Update(Action): 

1678 """Action to update ASG configuration settings 

1679 

1680 :example: 

1681 

1682 .. code-block:: yaml 

1683 

1684 policies: 

1685 - name: set-asg-instance-lifetime 

1686 resource: asg 

1687 filters: 

1688 - MaxInstanceLifetime: empty 

1689 actions: 

1690 - type: update 

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

1692 

1693 - name: set-asg-by-policy 

1694 resource: asg 

1695 actions: 

1696 - type: update 

1697 default-cooldown: 600 

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

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

1700 capacity-rebalance: true 

1701 """ 

1702 

1703 schema = type_schema( 

1704 'update', 

1705 **{ 

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

1707 'max-instance-lifetime': { 

1708 "anyOf": [ 

1709 {'enum': [0]}, 

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

1711 ] 

1712 }, 

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

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

1715 } 

1716 ) 

1717 permissions = ("autoscaling:UpdateAutoScalingGroup",) 

1718 settings_map = { 

1719 "default-cooldown": "DefaultCooldown", 

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

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

1722 "capacity-rebalance": "CapacityRebalance" 

1723 } 

1724 

1725 def validate(self): 

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

1727 raise PolicyValidationError( 

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

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

1730 ) 

1731 return self 

1732 

1733 def process(self, asgs): 

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

1735 

1736 settings = {} 

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

1738 if k in self.data: 

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

1740 

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

1742 futures = {} 

1743 error = None 

1744 for a in asgs: 

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

1746 for f in as_completed(futures): 

1747 if f.exception(): 

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

1749 futures[f]['AutoScalingGroupName'], 

1750 f.exception())) 

1751 error = f.exception() 

1752 if error: 

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

1754 raise error 

1755 

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

1757 self.manager.retry( 

1758 client.update_auto_scaling_group, 

1759 AutoScalingGroupName=asg['AutoScalingGroupName'], 

1760 **settings) 

1761 

1762 

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

1764class LaunchConfig(query.QueryResourceManager): 

1765 

1766 class resource_type(query.TypeInfo): 

1767 service = 'autoscaling' 

1768 arn_type = 'launchConfiguration' 

1769 id = name = 'LaunchConfigurationName' 

1770 date = 'CreatedTime' 

1771 enum_spec = ( 

1772 'describe_launch_configurations', 'LaunchConfigurations', None) 

1773 filter_name = 'LaunchConfigurationNames' 

1774 filter_type = 'list' 

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

1776 

1777 

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

1779class LaunchConfigAge(AgeFilter): 

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

1781 

1782 :example: 

1783 

1784 .. code-block:: yaml 

1785 

1786 policies: 

1787 - name: asg-launch-config-old 

1788 resource: launch-config 

1789 filters: 

1790 - type: age 

1791 days: 90 

1792 op: ge 

1793 """ 

1794 

1795 date_attribute = "CreatedTime" 

1796 schema = type_schema( 

1797 'age', 

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

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

1800 

1801 

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

1803class UnusedLaunchConfig(Filter): 

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

1805 

1806 :example: 

1807 

1808 .. code-block:: yaml 

1809 

1810 policies: 

1811 - name: asg-unused-launch-config 

1812 resource: launch-config 

1813 filters: 

1814 - unused 

1815 """ 

1816 

1817 schema = type_schema('unused') 

1818 

1819 def get_permissions(self): 

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

1821 

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

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

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

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

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

1827 

1828 

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

1830class LaunchConfigDelete(Action): 

1831 """Filters all unused launch configurations 

1832 

1833 :example: 

1834 

1835 .. code-block:: yaml 

1836 

1837 policies: 

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

1839 resource: launch-config 

1840 filters: 

1841 - unused 

1842 actions: 

1843 - delete 

1844 """ 

1845 

1846 schema = type_schema('delete') 

1847 permissions = ("autoscaling:DeleteLaunchConfiguration",) 

1848 

1849 def process(self, configs): 

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

1851 

1852 for c in configs: 

1853 self.process_config(client, c) 

1854 

1855 def process_config(self, client, config): 

1856 try: 

1857 client.delete_launch_configuration( 

1858 LaunchConfigurationName=config[ 

1859 'LaunchConfigurationName']) 

1860 except ClientError as e: 

1861 # Catch already deleted 

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

1863 return 

1864 raise 

1865 

1866 

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

1868class ScalingPolicy(query.QueryResourceManager): 

1869 

1870 class resource_type(query.TypeInfo): 

1871 service = 'autoscaling' 

1872 arn_type = "scalingPolicy" 

1873 id = name = 'PolicyName' 

1874 date = 'CreatedTime' 

1875 enum_spec = ( 

1876 'describe_policies', 'ScalingPolicies', None 

1877 ) 

1878 filter_name = 'PolicyNames' 

1879 filter_type = 'list' 

1880 cfn_type = 'AWS::AutoScaling::ScalingPolicy' 

1881 

1882 

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

1884class ScalingPolicyFilter(ValueFilter): 

1885 

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

1887 

1888 :example: 

1889 

1890 .. code-block:: yaml 

1891 

1892 policies: 

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

1894 resource: asg 

1895 filters: 

1896 - type: scaling-policy 

1897 key: PolicyType 

1898 value: "TargetTrackingScaling" 

1899 

1900 """ 

1901 

1902 schema = type_schema( 

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

1904 ) 

1905 schema_alias = False 

1906 permissions = ("autoscaling:DescribePolicies",) 

1907 annotate = False # no default value annotation on policy 

1908 annotation_key = 'c7n:matched-policies' 

1909 

1910 def get_scaling_policies(self, asgs): 

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

1912 policy_map = {} 

1913 for policy in policies: 

1914 policy_map.setdefault( 

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

1916 return policy_map 

1917 

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

1919 self.policy_map = self.get_scaling_policies(asgs) 

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

1921 

1922 def __call__(self, asg): 

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

1924 matched = [] 

1925 for policy in asg_policies: 

1926 if self.match(policy): 

1927 matched.append(policy) 

1928 if matched: 

1929 asg[self.annotation_key] = matched 

1930 return bool(matched)