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
« 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
5from collections import Counter
6from concurrent.futures import as_completed
8from dateutil.parser import parse
10import itertools
11import time
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
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)
27from .ec2 import deserialize_user_data
30@resources.register('asg')
31class ASG(query.QueryResourceManager):
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'
47 default_report_fields = (
48 'AutoScalingGroupName',
49 'CreatedTime',
50 'LaunchConfigurationName',
51 'count:Instances',
52 'DesiredCapacity',
53 'HealthCheckType',
54 'list:LoadBalancerNames',
55 )
57 retry = staticmethod(get_retry(('ResourceInUse', 'Throttling',)))
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)
67class LaunchInfo:
69 permissions = ("ec2:DescribeLaunchTemplateVersions",
70 "autoscaling:DescribeLaunchConfigurations",)
72 def __init__(self, manager):
73 self.manager = manager
75 def initialize(self, asgs):
76 self.templates = self.get_launch_templates(asgs)
77 self.configs = self.get_launch_configs(asgs)
78 return self
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)}
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}
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()
115 lid = asg.get('LaunchTemplate')
116 if lid is not None:
117 return (lid['LaunchTemplateId'], lid.get('Version', '$Default'))
119 if 'MixedInstancesPolicy' in asg:
120 mip_spec = asg['MixedInstancesPolicy'][
121 'LaunchTemplate']['LaunchTemplateSpecification']
122 return (mip_spec['LaunchTemplateId'], mip_spec.get('Version', '$Default'))
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
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)
136 def items(self):
137 return itertools.chain(*(
138 self.configs.items(), self.templates.items()))
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
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)}
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
179@ASG.filter_registry.register('security-group')
180class SecurityGroupFilter(net_filters.SecurityGroupFilter):
182 RelatedIdsExpression = ""
184 permissions = ('ec2:DescribeSecurityGroups',) + LaunchInfo.permissions
186 def get_related_ids(self, asgs):
187 return self.launch_info.get_security_group_ids()
189 def process(self, asgs, event=None):
190 self.launch_info = LaunchInfo(self.manager).initialize(asgs)
191 return super(SecurityGroupFilter, self).process(asgs, event)
194@ASG.filter_registry.register('subnet')
195class SubnetFilter(net_filters.SubnetFilter):
197 RelatedIdsExpression = ""
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
207@ASG.filter_registry.register('launch-config')
208class LaunchConfigFilter(ValueFilter):
209 """Filter asg by launch config attributes.
211 This will also filter to launch template data in addition
212 to launch configurations.
214 :example:
216 .. code-block:: yaml
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",)
231 def process(self, asgs, event=None):
232 self.launch_info = LaunchInfo(self.manager).initialize(asgs)
233 return super(LaunchConfigFilter, self).process(asgs, event)
235 def __call__(self, asg):
236 return self.match(self.launch_info.get(asg))
239class ConfigValidFilter(Filter):
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')]))
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
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()
264 def get_subnets(self):
265 manager = self.manager.get_resource_manager('subnet')
266 return {s['SubnetId'] for s in manager.resources()}
268 def get_security_groups(self):
269 manager = self.manager.get_resource_manager('security-group')
270 return {s['GroupId'] for s in manager.resources()}
272 def get_key_pairs(self):
273 manager = self.manager.get_resource_manager('key-pair')
274 return {k['KeyName'] for k in manager.resources()}
276 def get_elbs(self):
277 manager = self.manager.get_resource_manager('elb')
278 return {e['LoadBalancerName'] for e in manager.resources()}
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()}
284 def get_images(self):
285 images = self.launch_info.get_image_map()
286 image_snaps = set()
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
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)}
309 def process(self, asgs, event=None):
310 self.initialize(asgs)
311 return super(ConfigValidFilter, self).process(asgs, event)
313 def get_asg_errors(self, asg):
314 errors = []
315 subnets = asg.get('VPCZoneIdentifier', '').split(',')
317 for subnet in subnets:
318 subnet = subnet.strip()
319 if subnet not in self.subnets:
320 errors.append(('invalid-subnet', subnet))
322 for elb in asg['LoadBalancerNames']:
323 elb = elb.strip()
324 if elb not in self.elbs:
325 errors.append(('invalid-elb', elb))
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))
332 cfg_id = self.launch_info.get_launch_id(asg)
333 cfg = self.launch_info.get(asg)
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
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))
348 if cfg.get('KeyName') and cfg['KeyName'].strip() not in self.key_pairs:
349 errors.append(('invalid-key-pair', cfg['KeyName']))
351 if cfg.get('ImageId') and cfg['ImageId'].strip() not in self.images:
352 errors.append(('invalid-image', cfg['ImageId']))
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
365@ASG.filter_registry.register('valid')
366class ValidConfigFilter(ConfigValidFilter):
367 """Filters autoscale groups to find those that are structurally valid.
369 This operates as the inverse of the invalid filter for multi-step
370 workflows.
372 See details on the invalid filter for a list of checks made.
374 :example:
376 .. code-block:: yaml
378 policies:
379 - name: asg-valid-config
380 resource: asg
381 filters:
382 - valid
383 """
385 schema = type_schema('valid')
387 def __call__(self, asg):
388 errors = self.get_asg_errors(asg)
389 return not bool(errors)
392@ASG.filter_registry.register('invalid')
393class InvalidConfigFilter(ConfigValidFilter):
394 """Filter autoscale groups to find those that are structurally invalid.
396 Structurally invalid means that the auto scale group will not be able
397 to launch an instance succesfully as the configuration has
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)
406 Internally this tries to reuse other resource managers for better
407 cache utilization.
409 :example:
411 .. code-block:: yaml
413 policies:
414 - name: asg-invalid-config
415 resource: asg
416 filters:
417 - invalid
418 """
419 schema = type_schema('invalid')
421 def __call__(self, asg):
422 errors = self.get_asg_errors(asg)
423 if errors:
424 asg['Invalid'] = errors
425 return True
428@ASG.filter_registry.register('not-encrypted')
429class NotEncryptedFilter(Filter):
430 """Check if an ASG is configured to have unencrypted volumes.
432 Checks both the ami snapshots and the launch configuration.
434 :example:
436 .. code-block:: yaml
438 policies:
439 - name: asg-unencrypted
440 resource: asg
441 filters:
442 - type: not-encrypted
443 exclude_image: true
444 """
446 schema = type_schema('not-encrypted', exclude_image={'type': 'boolean'})
447 permissions = (
448 'ec2:DescribeImages',
449 'ec2:DescribeSnapshots',
450 'autoscaling:DescribeLaunchConfigurations')
452 images = unencrypted_configs = unencrypted_images = None
454 # TODO: resource-manager, notfound err mgr
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()
460 if not self.data.get('exclude_image'):
461 self.unencrypted_images = self.get_unencrypted_images()
463 self.unencrypted_launch = self.get_unencrypted_configs()
464 return super(NotEncryptedFilter, self).process(asgs, event)
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
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')
480 if launch_id in self.unencrypted_launch:
481 unencrypted.append('LaunchConfig')
482 if unencrypted:
483 asg['Unencrypted'] = unencrypted
484 return bool(unencrypted)
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
496 def get_unencrypted_configs(self):
497 """retrieve configs that have unencrypted ebs voluems referenced."""
498 unencrypted_configs = set()
499 snaps = {}
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
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
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)
536@ASG.filter_registry.register('image-age')
537class ImageAgeFilter(AgeFilter):
538 """Filter asg by image age (in days).
540 :example:
542 .. code-block:: yaml
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")
556 date_attribute = "CreationDate"
557 schema = type_schema(
558 'image-age',
559 op={'$ref': '#/definitions/filters_common/comparison_operators'},
560 days={'type': 'number'})
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)
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"))
576@ASG.filter_registry.register('image')
577class ImageFilter(ValueFilter):
578 """Filter asg by image
580 :example:
582 .. code-block:: yaml
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")
597 schema = type_schema('image', rinherit=ValueFilter.schema)
598 schema_alias = True
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)
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)
618@ASG.filter_registry.register('vpc-id')
619class VpcIdFilter(ValueFilter):
620 """Filters ASG based on the VpcId
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.
625 :example:
627 .. code-block:: yaml
629 policies:
630 - name: asg-vpc-xyz
631 resource: asg
632 filters:
633 - type: vpc-id
634 value: vpc-12ab34cd
635 """
637 schema = type_schema(
638 'vpc-id', rinherit=ValueFilter.schema)
639 schema['properties'].pop('key')
640 schema_alias = False
641 permissions = ('ec2:DescribeSubnets',)
643 # TODO: annotation
645 def __init__(self, data, manager=None):
646 super(VpcIdFilter, self).__init__(data, manager)
647 self.data['key'] = 'VpcId'
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)
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()}
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)
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
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.
682 :example:
684 .. code-block:: yaml
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")
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
727@ASG.action_registry.register('post-finding')
728class AsgPostFinding(PostFinding):
730 resource_type = 'AwsAutoScalingAutoScalingGroup'
731 launch_info = LaunchInfo(None)
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
747@ASG.action_registry.register('auto-tag-user')
748class AutoScaleAutoTagUser(AutoTagUser):
750 schema = type_schema(
751 'auto-tag-user',
752 propagate={'type': 'boolean'},
753 rinherit=AutoTagUser.schema)
754 schema_alias = False
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)
763@ASG.action_registry.register('tag-trim')
764class GroupTagTrim(TagTrim):
765 """Action to trim the number of tags to avoid hitting tag limits
767 :example:
769 .. code-block:: yaml
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 """
785 max_tag_count = 10
786 permissions = ('autoscaling:DeleteTags',)
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)
797@ASG.filter_registry.register('capacity-delta')
798class CapacityDelta(Filter):
799 """Filter returns ASG that have less instances than desired or required
801 :example:
803 .. code-block:: yaml
805 policies:
806 - name: asg-capacity-delta
807 resource: asg
808 filters:
809 - capacity-delta
810 """
812 schema = type_schema('capacity-delta')
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']]
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.
827 :example:
829 .. code-block:: yaml
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 """
842 schema = type_schema('user-data', rinherit=ValueFilter.schema)
843 schema_alias = False
844 batch_size = 50
845 annotation = 'c7n:user-data'
847 def __init__(self, data, manager):
848 super(UserDataFilter, self).__init__(data, manager)
849 self.data['key'] = '"c7n:user-data"'
851 def get_permissions(self):
852 return self.manager.get_resource_manager('asg').get_permissions()
854 def process(self, asgs, event=None):
855 '''Get list of autoscaling groups whose launch configs match the
856 user-data filter.
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)
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
877@ASG.action_registry.register('resize')
878class Resize(Action):
879 """Action to resize the min/max/desired instances in an ASG
881 There are several ways to use this action:
883 1. set min/desired to current running instances
885 .. code-block:: yaml
887 policies:
888 - name: asg-resize
889 resource: asg
890 filters:
891 - capacity-delta
892 actions:
893 - type: resize
894 desired-size: "current"
896 2. apply a fixed resize of min, max or desired, optionally saving the
897 previous values to a named tag (for restoring later):
899 .. code-block:: yaml
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
914 3. restore previous values for min/max/desired from a tag:
916 .. code-block:: yaml
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
929 """
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 )
958 def process(self, asgs):
959 # ASG parameters to save to/restore from a tag
960 asg_params = ['MinSize', 'MaxSize', 'DesiredCapacity']
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']
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'])
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)
984 else:
985 # we want to resize, parse provided params
986 if 'min-size' in self.data:
987 update['MinSize'] = self.data['min-size']
989 if 'max-size' in self.data:
990 update['MaxSize'] = self.data['max-size']
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']
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']))
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)
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')
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
1036 :example:
1038 .. code-block:: yaml
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 """
1050 schema = type_schema(
1051 'remove-tag',
1052 aliases=('untag', 'unmark'),
1053 tags={'type': 'array', 'items': {'type': 'string'}},
1054 key={'type': 'string'})
1056 permissions = ('autoscaling:DeleteTags',)
1057 batch_size = 1
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')
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
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)
1094@ASG.action_registry.register('tag')
1095@ASG.action_registry.register('mark')
1096class Tag(Action):
1097 """Action to add a tag to an ASG
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.
1103 :example:
1105 .. code-block:: yaml
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 """
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
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})
1142 for k, v in self.data.get('tags', {}).items():
1143 tags.append({'Key': k, 'Value': v})
1145 return tags
1147 def process(self, asgs):
1148 tags = self.get_tag_set()
1149 error = None
1151 self.interpolate_values(tags)
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
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)
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)
1194 def get_client(self):
1195 return local_session(self.manager.session_factory).client('autoscaling')
1198@ASG.action_registry.register('propagate-tags')
1199class PropagateTags(Action):
1200 """Propagate tags to an asg instances.
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.
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.
1210 :example:
1212 .. code-block:: yaml
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
1224 """
1226 schema = type_schema(
1227 'propagate-tags',
1228 tags={'type': 'array', 'items': {'type': 'string'}},
1229 trim={'type': 'boolean'})
1230 permissions = ('ec2:DeleteTags', 'ec2:CreateTags')
1232 def validate(self):
1233 if not isinstance(self.data.get('tags', []), (list, tuple)):
1234 raise ValueError("No tags specified")
1235 return self
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)
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:')}
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']}
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')))
1261 tag_set = set(tag_map)
1262 client = local_session(self.manager.session_factory).client('ec2')
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)
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)
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)
1281 remove_tags = []
1282 extra_tags = []
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)
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)
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])
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)}
1322@ASG.action_registry.register('rename-tag')
1323class RenameTag(Action):
1324 """Rename a tag on an AutoScaleGroup.
1326 :example:
1328 .. code-block:: yaml
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 """
1342 schema = type_schema(
1343 'rename-tag', required=['source', 'dest'],
1344 propagate={'type': 'boolean'},
1345 source={'type': 'string'},
1346 dest={'type': 'string'})
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
1356 def process(self, asgs):
1357 source = self.data.get('source')
1358 dest = self.data.get('dest')
1359 count = len(asgs)
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))
1374 def process_asg(self, asg):
1375 """Move source tag to destination tag.
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)
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']}])
1419@ASG.action_registry.register('mark-for-op')
1420class MarkForOp(TagDelayedAction):
1421 """Action to create a delayed action for a later date
1423 :example:
1425 .. code-block:: yaml
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 """
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}')
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
1469@ASG.action_registry.register('suspend')
1470class Suspend(Action):
1471 """Action to suspend ASG processes and instances
1473 AWS ASG suspend/resume and process docs
1474 https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-suspend-resume-processes.html
1476 :example:
1478 .. code-block:: yaml
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")
1490 ASG_PROCESSES = [
1491 "Launch",
1492 "Terminate",
1493 "HealthCheck",
1494 "ReplaceUnhealthy",
1495 "AZRebalance",
1496 "AlarmNotification",
1497 "ScheduledActions",
1498 "AddToLoadBalancer",
1499 "InstanceRefresh"]
1501 schema = type_schema(
1502 'suspend',
1503 exclude={
1504 'type': 'array',
1505 'title': 'ASG Processes to not suspend',
1506 'items': {'enum': ASG_PROCESSES}})
1508 ASG_PROCESSES = set(ASG_PROCESSES)
1510 def process(self, asgs):
1511 with self.executor_factory(max_workers=3) as w:
1512 list(w.map(self.process_asg, asgs))
1514 def process_asg(self, asg):
1515 """Multistep process to stop an asg aprori of setup
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', ())))
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
1553@ASG.action_registry.register('resume')
1554class Resume(Action):
1555 """Resume a suspended autoscale group and its instances
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)
1562 :example:
1564 .. code-block:: yaml
1566 policies:
1567 - name: asg-resume-processes
1568 resource: asg
1569 filters:
1570 - "tag:Resume": present
1571 actions:
1572 - type: resume
1573 delay: 300
1575 """
1576 schema = type_schema('resume', delay={'type': 'number'})
1577 permissions = ("autoscaling:ResumeProcesses", "ec2:StartInstances")
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))
1586 session = local_session(self.manager.session_factory)
1587 ec2_client = session.client('ec2')
1588 asg_client = session.client('autoscaling')
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
1601 self.log.debug("Sleeping for asg health check grace")
1602 time.sleep(self.delay)
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()))
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)
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'])
1632@ASG.action_registry.register('delete')
1633class Delete(Action):
1634 """Action to delete an ASG
1636 The 'force' parameter is needed when deleting an ASG that has instances
1637 attached to it.
1639 :example:
1641 .. code-block:: yaml
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 """
1654 schema = type_schema('delete', force={'type': 'boolean'})
1655 permissions = ("autoscaling:DeleteAutoScalingGroup",)
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)
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
1676@ASG.action_registry.register('update')
1677class Update(Action):
1678 """Action to update ASG configuration settings
1680 :example:
1682 .. code-block:: yaml
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)
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 """
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 }
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
1733 def process(self, asgs):
1734 client = local_session(self.manager.session_factory).client('autoscaling')
1736 settings = {}
1737 for k, v in self.settings_map.items():
1738 if k in self.data:
1739 settings[v] = self.data.get(k)
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
1756 def process_asg(self, client, asg, settings):
1757 self.manager.retry(
1758 client.update_auto_scaling_group,
1759 AutoScalingGroupName=asg['AutoScalingGroupName'],
1760 **settings)
1763@resources.register('launch-config')
1764class LaunchConfig(query.QueryResourceManager):
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'
1778@LaunchConfig.filter_registry.register('age')
1779class LaunchConfigAge(AgeFilter):
1780 """Filter ASG launch configuration by age (in days)
1782 :example:
1784 .. code-block:: yaml
1786 policies:
1787 - name: asg-launch-config-old
1788 resource: launch-config
1789 filters:
1790 - type: age
1791 days: 90
1792 op: ge
1793 """
1795 date_attribute = "CreatedTime"
1796 schema = type_schema(
1797 'age',
1798 op={'$ref': '#/definitions/filters_common/comparison_operators'},
1799 days={'type': 'number'})
1802@LaunchConfig.filter_registry.register('unused')
1803class UnusedLaunchConfig(Filter):
1804 """Filters all launch configurations that are not in use but exist
1806 :example:
1808 .. code-block:: yaml
1810 policies:
1811 - name: asg-unused-launch-config
1812 resource: launch-config
1813 filters:
1814 - unused
1815 """
1817 schema = type_schema('unused')
1819 def get_permissions(self):
1820 return self.manager.get_resource_manager('asg').get_permissions()
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]
1829@LaunchConfig.action_registry.register('delete')
1830class LaunchConfigDelete(Action):
1831 """Filters all unused launch configurations
1833 :example:
1835 .. code-block:: yaml
1837 policies:
1838 - name: asg-unused-launch-config-delete
1839 resource: launch-config
1840 filters:
1841 - unused
1842 actions:
1843 - delete
1844 """
1846 schema = type_schema('delete')
1847 permissions = ("autoscaling:DeleteLaunchConfiguration",)
1849 def process(self, configs):
1850 client = local_session(self.manager.session_factory).client('autoscaling')
1852 for c in configs:
1853 self.process_config(client, c)
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
1867@resources.register('scaling-policy')
1868class ScalingPolicy(query.QueryResourceManager):
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'
1883@ASG.filter_registry.register('scaling-policy')
1884class ScalingPolicyFilter(ValueFilter):
1886 """Filter asg by scaling-policies attributes.
1888 :example:
1890 .. code-block:: yaml
1892 policies:
1893 - name: scaling-policies-with-target-tracking
1894 resource: asg
1895 filters:
1896 - type: scaling-policy
1897 key: PolicyType
1898 value: "TargetTrackingScaling"
1900 """
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'
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
1918 def process(self, asgs, event=None):
1919 self.policy_map = self.get_scaling_policies(asgs)
1920 return super(ScalingPolicyFilter, self).process(asgs, event)
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)