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)